From f225fb0138ac0c8ac6adb382874b37534fe35967 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Jun 2023 14:52:52 +1000 Subject: [PATCH 01/63] Fix EE9 ContextHandler class loading when it is not the root handler. Signed-off-by: Lachlan Roberts --- .../jetty/ee9/nested/ContextHandler.java | 78 ++++++++++++++++--- .../jetty/ee9/webapp/WebAppContext.java | 5 -- .../jetty/ee9/webapp/WebAppContextTest.java | 62 +++++++++++++++ 3 files changed, 131 insertions(+), 14 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 daedccd4197..cf951e5aabf 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 @@ -91,6 +91,7 @@ import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.DumpableCollection; import org.eclipse.jetty.util.component.Environment; import org.eclipse.jetty.util.component.Graceful; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.Resources; @@ -230,6 +231,12 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu this(null, null, contextPath); } + public ContextHandler(String contextPath, org.eclipse.jetty.ee9.nested.Handler handler) + { + this(contextPath); + setHandler(handler); + } + public ContextHandler(org.eclipse.jetty.server.Handler.Container parent) { this(null, parent, "/"); @@ -296,6 +303,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu public void setServer(Server server) { super.setServer(server); + if (!Objects.equals(server, _coreContextHandler.getServer())) + _coreContextHandler.setServer(server); if (_errorHandler != null) _errorHandler.setServer(server); } @@ -603,12 +612,49 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu protected void doStart() throws Exception { // If we are being started directly (rather than via a start of the CoreContextHandler), then - // we need to run ourselves in the core context + // we need to run ourselves in the core context. if (org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext() != _coreContextHandler.getContext()) { + // Make the CoreContextHandler lifecycle responsible for calling the doStartContext() and doStopContext(). _coreContextHandler.unmanage(this); + unmanage(_coreContextHandler); + _coreContextHandler.addEventListener(new LifeCycle.Listener() + { + @Override + public void lifeCycleStarting(LifeCycle event) + { + try + { + _coreContextHandler.getContext().call(() -> doStartInContext(), null); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + @Override + public void lifeCycleStopping(LifeCycle event) + { + try + { + _coreContextHandler.getContext().call(() -> doStopInContext(), null); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + @Override + public void lifeCycleStopped(LifeCycle event) + { + _coreContextHandler.removeEventListener(this); + } + }); + _coreContextHandler.start(); - _coreContextHandler.manage(this); + return; } _coreContextHandler.getContext().call(this::doStartInContext, null); @@ -634,14 +680,14 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu @Override protected void doStop() throws Exception { - // If we are being stopped directly (rather than via a start of the CoreContextHandler), then - // we need to stop ourselves in the core context + // If we are being stopped directly (rather than via a start of the CoreContextHandler), + // then doStopInContext() will be called by the listener on the lifecycle of CoreContextHandler. if (org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext() != _coreContextHandler.getContext()) { - _coreContextHandler.unmanage(this); _coreContextHandler.stop(); - _coreContextHandler.manage(this); + return; } + _coreContextHandler.getContext().call(this::doStopInContext, null); } @@ -827,7 +873,22 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (LOG.isDebugEnabled()) LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this); - nextScope(target, baseRequest, request, response); + try + { + org.eclipse.jetty.server.handler.ContextHandler.ScopedContext context = getCoreContextHandler().getContext(); + if (context == org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext()) + nextScope(target, baseRequest, request, response); + else + context.call(() -> nextScope(target, baseRequest, request, response), baseRequest.getCoreRequest()); + } + catch (IOException | ServletException e) + { + throw e; + } + catch (Throwable t) + { + throw new ServletException("Unexpected Exception", t); + } } protected void requestInitialized(Request baseRequest, HttpServletRequest request) @@ -2507,7 +2568,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu } @Override - protected CoreContext newContext() + protected ScopedContext newContext() { return new CoreContext(); } @@ -2566,7 +2627,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu public ContextHandler getContextHandler() { - return ContextHandler.this; } diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java index 55cc09338dc..2835ba0cef9 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java @@ -507,7 +507,6 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL @Override protected void doStart() throws Exception { - ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); try { _metadata.setAllowDuplicateFragmentNames(isAllowDuplicateFragmentNames()); @@ -531,10 +530,6 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL if (isThrowUnavailableOnStartupException()) throw t; } - finally - { - Thread.currentThread().setContextClassLoader(oldLoader); - } } private void wrapConfigurations() diff --git a/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java b/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java index d2ebe04cac7..62679431c8c 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java +++ b/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java @@ -29,6 +29,7 @@ import jakarta.servlet.GenericServlet; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import org.eclipse.jetty.ee9.nested.ContextHandler; import org.eclipse.jetty.ee9.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.ee9.servlet.ServletContextHandler; import org.eclipse.jetty.http.HttpStatus; @@ -158,6 +159,67 @@ public class WebAppContextTest assertEquals("/foo", wac.getContextPath()); } + @Test + public void nestedContextHandlerTest() throws Exception + { + Server server = newServer(); + File webXml = MavenTestingUtils.getTargetFile("test-classes/web-with-default-context-path.xml"); + File webXmlEmptyPath = MavenTestingUtils.getTargetFile("test-classes/web-with-empty-default-context-path.xml"); + File webDefaultXml = MavenTestingUtils.getTargetFile("test-classes/web-default-with-default-context-path.xml"); + File overrideWebXml = MavenTestingUtils.getTargetFile("test-classes/override-web-with-default-context-path.xml"); + assertNotNull(webXml); + assertNotNull(webDefaultXml); + assertNotNull(overrideWebXml); + assertNotNull(webXmlEmptyPath); + + WebAppContext wac = new WebAppContext(); + wac.setResourceBase(MavenTestingUtils.getTargetTestingDir().getAbsolutePath()); + + // Put WebAppContext inside another nested ContextHandler. + ContextHandler contextHandler = new ContextHandler("/", wac); + server.setHandler(contextHandler); + + // All components should be started even the webapp context CoreContextHandler. + assertNotNull(contextHandler.getServer()); + assertNotNull(contextHandler.getCoreContextHandler().getServer()); + assertNotNull(wac.getServer()); + assertNotNull(wac.getCoreContextHandler().getServer()); + + //test that an empty default-context-path defaults to root + wac.setDescriptor(webXmlEmptyPath.getAbsolutePath()); + server.start(); + assertEquals("/", wac.getContextPath()); + + server.stop(); + + //test web-default.xml value is used + wac.setDescriptor(null); + wac.setDefaultsDescriptor(webDefaultXml.getAbsolutePath()); + server.start(); + assertEquals("/one", wac.getContextPath()); + + server.stop(); + + //test web.xml value is used + wac.setDescriptor(webXml.getAbsolutePath()); + server.start(); + assertEquals("/two", wac.getContextPath()); + + server.stop(); + + //test override-web.xml value is used + wac.setOverrideDescriptor(overrideWebXml.getAbsolutePath()); + server.start(); + assertEquals("/three", wac.getContextPath()); + + server.stop(); + + //test that explicitly set context path is used instead + wac.setContextPath("/foo"); + server.start(); + assertEquals("/foo", wac.getContextPath()); + } + @Test public void testConfigurationClassesFromDefault() { From 24cb5dba4a7b397e4d6958c7c6af2d8f65ab02e3 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Jun 2023 15:21:31 +1000 Subject: [PATCH 02/63] Add test to reproduce classloader failures in 12.0.x. Signed-off-by: Lachlan Roberts --- .../ContextHandlerClassLoaderTest.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java new file mode 100644 index 00000000000..6a5addc0f45 --- /dev/null +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java @@ -0,0 +1,114 @@ +// +// ======================================================================== +// 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.ee9.servlet; + +import java.io.IOException; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.ee9.nested.ContextHandler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class ContextHandlerClassLoaderTest +{ + private ServletContextHandler _context; + private ServerConnector _connector; + private Server _server; + private HttpClient _client; + + public static class MyCustomClassLoader extends ClassLoader + { + } + + @BeforeEach + public void init() throws Exception + { + _server = new Server(); + _connector = new ServerConnector(_server); + _server.addConnector(_connector); + + _context = new ServletContextHandler(); + _context.setClassLoader(new MyCustomClassLoader()); + _context.addServlet(new ServletHolder(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getDispatcherType() == DispatcherType.REQUEST) + { + AsyncContext asyncContext = req.startAsync(); + asyncContext.start(asyncContext::dispatch); + return; + } + + resp.getWriter().print(req.getDispatcherType() + " " + Thread.currentThread().getContextClassLoader()); + if (req.isAsyncStarted()) + req.getAsyncContext().complete(); + } + }), "/"); + + _client = new HttpClient(); + _client.start(); + } + + @AfterEach + public void afterEach() throws Exception + { + _client.stop(); + _server.stop(); + } + + @Test + public void testRootContextHandler() throws Exception + { + _server.setHandler(_context); + _server.start(); + + ContentResponse response = _client.GET("http://localhost:" + _connector.getLocalPort()); + assertThat(response.getStatus(), equalTo(200)); + String responseContent = response.getContentAsString(); + assertThat(responseContent, containsString("ASYNC")); + assertThat(responseContent, containsString("MyCustomClassLoader")); + } + + @Test + public void testNestedContextHandler() throws Exception + { + ContextHandler contextHandler = new ContextHandler(); + contextHandler.setHandler(_context); + contextHandler.setContextPath("/"); + _server.setHandler(contextHandler); + _server.start(); + + ContentResponse response = _client.GET("http://localhost:" + _connector.getLocalPort()); + assertThat(response.getStatus(), equalTo(200)); + String responseContent = response.getContentAsString(); + assertThat(responseContent, containsString("ASYNC")); + assertThat(responseContent, containsString("MyCustomClassLoader")); + } +} From c96fdd728a6107ea5a72e71fd08cd29c6924ce28 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Jun 2023 15:29:38 +1000 Subject: [PATCH 03/63] cleanup unused variables in nested ContextHandler Signed-off-by: Lachlan Roberts --- .../main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java | 2 -- 1 file changed, 2 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 cf951e5aabf..ef72474d468 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 @@ -1008,8 +1008,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu */ public void handle(Request request, Runnable runnable) { - ClassLoader oldClassloader = null; - Thread currentThread = null; APIContext oldContext = __context.get(); // Are we already in the scope? From 7eebfc1955d6826c5e42eb1ce2cd99459d9e4b2b Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 7 Jun 2023 13:24:31 +1000 Subject: [PATCH 04/63] fixes for ContextHandler lifecycle when called directly Signed-off-by: Lachlan Roberts --- .../jetty/ee9/nested/ContextHandler.java | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 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 ef72474d468..c5440a461f3 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 @@ -617,7 +617,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu { // Make the CoreContextHandler lifecycle responsible for calling the doStartContext() and doStopContext(). _coreContextHandler.unmanage(this); - unmanage(_coreContextHandler); _coreContextHandler.addEventListener(new LifeCycle.Listener() { @Override @@ -631,25 +630,16 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu { throw new RuntimeException(e); } - } - - @Override - public void lifeCycleStopping(LifeCycle event) - { - try + finally { - _coreContextHandler.getContext().call(() -> doStopInContext(), null); - } - catch (Exception e) - { - throw new RuntimeException(e); + _coreContextHandler.removeEventListener(this); } } @Override - public void lifeCycleStopped(LifeCycle event) + public void lifeCycleStarted(LifeCycle event) { - _coreContextHandler.removeEventListener(this); + _coreContextHandler.manage(this); } }); @@ -684,6 +674,34 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu // then doStopInContext() will be called by the listener on the lifecycle of CoreContextHandler. if (org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext() != _coreContextHandler.getContext()) { + // Make the CoreContextHandler lifecycle responsible for calling the doStartContext() and doStopContext(). + _coreContextHandler.unmanage(this); + _coreContextHandler.addEventListener(new LifeCycle.Listener() + { + @Override + public void lifeCycleStopping(LifeCycle event) + { + try + { + _coreContextHandler.getContext().call(() -> doStopInContext(), null); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + finally + { + _coreContextHandler.removeEventListener(this); + } + } + + @Override + public void lifeCycleStopped(LifeCycle event) + { + _coreContextHandler.manage(this); + } + }); + _coreContextHandler.stop(); return; } From f3b925a893ccf993941828f63033473a06c0e2a1 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 9 Jun 2023 14:10:43 +1000 Subject: [PATCH 05/63] PR #9878 - changes from review Signed-off-by: Lachlan Roberts --- .../org/eclipse/jetty/ee9/nested/ContextHandler.java | 11 +++++++---- .../ee9/servlet/ContextHandlerClassLoaderTest.java | 6 +++++- 2 files changed, 12 insertions(+), 5 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 c5440a461f3..9d4ef7c029b 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 @@ -611,8 +611,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu @Override protected void doStart() throws Exception { - // If we are being started directly (rather than via a start of the CoreContextHandler), then - // we need to run ourselves in the core context. + // If we are being started directly (rather than via a start of the CoreContextHandler), + // then we need the LifeCycle Listener to ensure both this and the CoreContextHandler are + // in STARTING state when doStartInContext is called. if (org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext() != _coreContextHandler.getContext()) { // Make the CoreContextHandler lifecycle responsible for calling the doStartContext() and doStopContext(). @@ -2524,7 +2525,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu protected void doStart() throws Exception { ClassLoader old = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(ENVIRONMENT.getClassLoader()); + if (getClassLoader() != null) + Thread.currentThread().setContextClassLoader(getClassLoader()); try { super.doStart(); @@ -2548,7 +2550,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu protected void doStop() throws Exception { ClassLoader old = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(ENVIRONMENT.getClassLoader()); + if (getClassLoader() != null) + Thread.currentThread().setContextClassLoader(getClassLoader()); try { super.doStop(); diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java index 6a5addc0f45..4e0ff972f6e 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java @@ -43,6 +43,10 @@ public class ContextHandlerClassLoaderTest public static class MyCustomClassLoader extends ClassLoader { + protected MyCustomClassLoader(ClassLoader parent) + { + super(parent); + } } @BeforeEach @@ -53,7 +57,7 @@ public class ContextHandlerClassLoaderTest _server.addConnector(_connector); _context = new ServletContextHandler(); - _context.setClassLoader(new MyCustomClassLoader()); + _context.setClassLoader(new MyCustomClassLoader(_context.getClassLoader())); _context.addServlet(new ServletHolder(new HttpServlet() { @Override From 4c1cf7de64acd59ebd13ca935adfca17c3a46502 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 14 Jun 2023 11:41:24 +1000 Subject: [PATCH 06/63] PR #9878 - fixes for test failures Signed-off-by: Lachlan Roberts --- .../org/eclipse/jetty/ee9/nested/ContextHandler.java | 12 +++--------- .../org/eclipse/jetty/ee9/webapp/WebAppContext.java | 5 +++++ 2 files changed, 8 insertions(+), 9 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 0260a15f30a..20d4fe6dc4c 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 @@ -610,16 +610,13 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie { throw new RuntimeException(e); } - finally - { - _coreContextHandler.removeEventListener(this); - } } @Override public void lifeCycleStarted(LifeCycle event) { _coreContextHandler.manage(this); + _coreContextHandler.removeEventListener(this); } }); @@ -669,16 +666,13 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie { throw new RuntimeException(e); } - finally - { - _coreContextHandler.removeEventListener(this); - } } @Override public void lifeCycleStopped(LifeCycle event) { _coreContextHandler.manage(this); + _coreContextHandler.removeEventListener(this); } }); @@ -879,7 +873,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie else context.call(() -> nextScope(target, baseRequest, request, response), baseRequest.getCoreRequest()); } - catch (IOException | ServletException e) + catch (IOException | ServletException | RuntimeException e) { throw e; } diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java index 18e419f8996..5c27ba9d6e7 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java @@ -503,6 +503,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL @Override protected void doStart() throws Exception { + ClassLoader old = Thread.currentThread().getContextClassLoader(); try { _metadata.setAllowDuplicateFragmentNames(isAllowDuplicateFragmentNames()); @@ -526,6 +527,10 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL if (isThrowUnavailableOnStartupException()) throw t; } + finally + { + Thread.currentThread().setContextClassLoader(old); + } } private void wrapConfigurations() From e4c79c551cb9064c99bb9622e9b82f44457e50c6 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 15 Jun 2023 11:18:05 +1000 Subject: [PATCH 07/63] Scope the request to the current ContextHandlers ApiContext Signed-off-by: Lachlan Roberts --- .../jetty/ee9/nested/ContextHandler.java | 6 +++ .../org/eclipse/jetty/ee9/nested/Request.java | 10 ++-- .../ContextHandlerClassLoaderTest.java | 47 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 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 20d4fe6dc4c..8425feffba5 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 @@ -865,8 +865,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie if (LOG.isDebugEnabled()) LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this); + APIContext oldApiContext = baseRequest.getContext(); try { + baseRequest.setContext(_apiContext); org.eclipse.jetty.server.handler.ContextHandler.ScopedContext context = getCoreContextHandler().getContext(); if (context == org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext()) nextScope(target, baseRequest, request, response); @@ -881,6 +883,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie { throw new ServletException("Unexpected Exception", t); } + finally + { + baseRequest.setContext(oldApiContext); + } } protected void requestInitialized(Request baseRequest, HttpServletRequest request) 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 646eaf1df0d..0cd34bd43b5 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 @@ -157,7 +157,7 @@ public class Request implements HttpServletRequest } private final HttpChannel _channel; - private final ContextHandler.APIContext _context; + private ContextHandler.APIContext _context; private final List _requestAttributeListeners = new ArrayList<>(); private final HttpInput _input; private ContextHandler.CoreContextRequest _coreRequest; @@ -193,7 +193,6 @@ public class Request implements HttpServletRequest { _channel = channel; _input = input; - _context = channel.getContextHandler().getServletContext(); } public HttpFields getHttpFields() @@ -690,13 +689,18 @@ public class Request implements HttpServletRequest } /** - * @return The {@link ContextHandler.APIContext context} used for this request. Never null. + * @return The {@link ContextHandler.APIContext context} used for this request. */ public ContextHandler.APIContext getContext() { return _context; } + public void setContext(ContextHandler.APIContext context) + { + _context = context; + } + /** * @return The current {@link ContextHandler.APIContext context} used for this error handling for this request. If the request is asynchronous, * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java index 4e0ff972f6e..6348a22558e 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java @@ -14,8 +14,12 @@ package org.eclipse.jetty.ee9.servlet; import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Queue; import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; import jakarta.servlet.DispatcherType; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; @@ -40,6 +44,7 @@ public class ContextHandlerClassLoaderTest private ServerConnector _connector; private Server _server; private HttpClient _client; + private final Queue _asyncListenerLog = new ArrayDeque<>(); public static class MyCustomClassLoader extends ClassLoader { @@ -47,6 +52,39 @@ public class ContextHandlerClassLoaderTest { super(parent); } + + @Override + public String toString() + { + return MyCustomClassLoader.class.getSimpleName(); + } + } + + public class MyAsyncListener implements AsyncListener + { + @Override + public void onComplete(AsyncEvent event) throws IOException + { + _asyncListenerLog.add("onComplete(): " + Thread.currentThread().getContextClassLoader()); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException + { + _asyncListenerLog.add("onTimeout(): " + Thread.currentThread().getContextClassLoader()); + } + + @Override + public void onError(AsyncEvent event) throws IOException + { + _asyncListenerLog.add("onError(): " + Thread.currentThread().getContextClassLoader()); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException + { + _asyncListenerLog.add("onStartAsync(): " + Thread.currentThread().getContextClassLoader()); + } } @BeforeEach @@ -66,6 +104,7 @@ public class ContextHandlerClassLoaderTest if (req.getDispatcherType() == DispatcherType.REQUEST) { AsyncContext asyncContext = req.startAsync(); + asyncContext.addListener(new MyAsyncListener()); asyncContext.start(asyncContext::dispatch); return; } @@ -98,6 +137,10 @@ public class ContextHandlerClassLoaderTest String responseContent = response.getContentAsString(); assertThat(responseContent, containsString("ASYNC")); assertThat(responseContent, containsString("MyCustomClassLoader")); + + // AsyncListener should also have the correct ClassLoader set. + assertThat(_asyncListenerLog.poll(), equalTo("onComplete(): MyCustomClassLoader")); + assertThat(_asyncListenerLog.size(), equalTo(0)); } @Test @@ -114,5 +157,9 @@ public class ContextHandlerClassLoaderTest String responseContent = response.getContentAsString(); assertThat(responseContent, containsString("ASYNC")); assertThat(responseContent, containsString("MyCustomClassLoader")); + + // AsyncListener should also have the correct ClassLoader set. + assertThat(_asyncListenerLog.poll(), equalTo("onComplete(): MyCustomClassLoader")); + assertThat(_asyncListenerLog.size(), equalTo(0)); } } From 5dcf022d6679bb11fc6c6e2a3c7b80973df88052 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 16 Jun 2023 07:39:48 +1000 Subject: [PATCH 08/63] use Request.setContext in all places done in Jetty-11 Signed-off-by: Lachlan Roberts --- .../jetty/ee9/nested/ContextHandler.java | 60 ++++++++++++++----- .../eclipse/jetty/ee9/nested/Dispatcher.java | 6 +- .../org/eclipse/jetty/ee9/nested/Request.java | 7 +-- .../jetty/ee9/nested/ResponseTest.java | 11 +++- 4 files changed, 60 insertions(+), 24 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 8425feffba5..c270128d6cb 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 @@ -865,15 +865,52 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie if (LOG.isDebugEnabled()) LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this); - APIContext oldApiContext = baseRequest.getContext(); + APIContext oldContext = baseRequest.getContext(); + String oldPathInContext = baseRequest.getPathInContext(); + String pathInContext = target; + DispatcherType dispatch = baseRequest.getDispatcherType(); + + // Are we already in this context? + if (oldContext != _apiContext) + { + // check the target. + String contextPath = getContextPath(); + if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) + { + if (target.length() > contextPath.length()) + { + if (contextPath.length() > 1) + target = target.substring(contextPath.length()); + pathInContext = target; + } + else if (contextPath.length() == 1) + { + target = "/"; + pathInContext = "/"; + } + else + { + target = "/"; + pathInContext = null; + } + } + } + try { - baseRequest.setContext(_apiContext); + baseRequest.setContext(_apiContext, + (DispatcherType.INCLUDE.equals(dispatch) || !target.startsWith("/")) ? oldPathInContext : pathInContext); + org.eclipse.jetty.server.handler.ContextHandler.ScopedContext context = getCoreContextHandler().getContext(); if (context == org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext()) + { nextScope(target, baseRequest, request, response); + } else - context.call(() -> nextScope(target, baseRequest, request, response), baseRequest.getCoreRequest()); + { + String t = target; + context.call(() -> nextScope(t, baseRequest, request, response), baseRequest.getCoreRequest()); + } } catch (IOException | ServletException | RuntimeException e) { @@ -885,7 +922,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie } finally { - baseRequest.setContext(oldApiContext); + baseRequest.setContext(oldContext, oldPathInContext); } } @@ -1660,14 +1697,12 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie // this is a dispatch with either a provided URI and/or a dispatched path // We will have to modify the request and then revert final HttpURI oldUri = baseRequest.getHttpURI(); - final String oldPathInContext = baseRequest.getPathInContext(); - final ServletPathMapping oldServletPathMapping = baseRequest.getServletPathMapping(); final MultiMap oldQueryParams = baseRequest.getQueryParameters(); try { if (encodedPathQuery == null) { - baseRequest.onDispatch(baseUri, oldPathInContext); + baseRequest.setHttpURI(baseUri); } else { @@ -1692,23 +1727,18 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie builder.param(baseUri.getParam()); if (StringUtil.isEmpty(builder.getQuery())) builder.query(baseUri.getQuery()); + baseRequest.setHttpURI(builder); - HttpURI uri = builder.asImmutable(); - String pathInContext = uri.getDecodedPath(); - if (baseRequest.getContextPath().length() > 1) - pathInContext = pathInContext.substring(baseRequest.getContextPath().length()); - - baseRequest.onDispatch(uri, pathInContext); if (baseUri.getQuery() != null && baseRequest.getQueryString() != null) baseRequest.mergeQueryParameters(oldUri.getQuery(), baseRequest.getQueryString()); } + baseRequest.setContext(null, baseRequest.getHttpURI().getDecodedPath()); handleAsync(channel, event, baseRequest); } finally { - baseRequest.onDispatch(oldUri, oldPathInContext); - baseRequest.setServletPathMapping(oldServletPathMapping); + baseRequest.setHttpURI(oldUri); baseRequest.setQueryParameters(oldQueryParams); baseRequest.resetParameters(); } diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Dispatcher.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Dispatcher.java index f539d6bfbc2..fb496b78d70 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Dispatcher.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Dispatcher.java @@ -188,7 +188,8 @@ public class Dispatcher implements RequestDispatcher query = old_uri.getQuery(); String decodedPathInContext = URIUtil.decodePath(_pathInContext); - baseRequest.onDispatch(HttpURI.build(old_uri, _uri.getPath(), _uri.getParam(), query), decodedPathInContext); + baseRequest.setHttpURI(HttpURI.build(old_uri, _uri.getPath(), _uri.getParam(), query)); + baseRequest.setContext(_contextHandler.getServletContext(), decodedPathInContext); if (_uri.getQuery() != null || old_uri.getQuery() != null) { @@ -229,7 +230,8 @@ public class Dispatcher implements RequestDispatcher } finally { - baseRequest.onDispatch(old_uri, old_path_in_context); + baseRequest.setHttpURI(old_uri); + baseRequest.setContext(old_context, old_path_in_context); baseRequest.setServletPathMapping(old_mapping); baseRequest.setQueryParameters(old_query_params); baseRequest.resetParameters(); 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 0cd34bd43b5..d28f837a9dd 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 @@ -696,9 +696,10 @@ public class Request implements HttpServletRequest return _context; } - public void setContext(ContextHandler.APIContext context) + public void setContext(ContextHandler.APIContext context, String pathInContext) { _context = context; + _pathInContext = pathInContext; } /** @@ -1344,13 +1345,11 @@ public class Request implements HttpServletRequest return _uri; } - public void onDispatch(HttpURI uri, String decodedPathInContext) + public void setHttpURI(HttpURI uri) { if (_uri != null && !Objects.equals(_uri.getQuery(), uri.getQuery()) && _queryParameters != BAD_PARAMS) _parameters = _queryParameters = null; _uri = uri.asImmutable(); - _pathInContext = decodedPathInContext; - _servletPathMapping = null; } /** diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java index cefbae90d2d..7bd745a320f 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java +++ b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java @@ -76,6 +76,7 @@ import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.util.thread.TimerScheduler; import org.hamcrest.Matchers; @@ -1605,7 +1606,8 @@ public class ResponseTest Response response = getResponse(); Request request = response.getHttpChannel().getRequest(); - request.onDispatch(HttpURI.build(request.getHttpURI()).host("myhost").port(8888), "/path/info"); + request.setHttpURI(HttpURI.build(request.getHttpURI()).host("myhost").port(8888)); + request.setContext(_context._apiContext, "/path/info"); assertEquals("http://myhost:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/path/info;param?query=0&more=1#target")); @@ -1716,7 +1718,8 @@ public class ResponseTest uri.scheme("http"); if (host != null) uri.host(host).port(port); - request.onDispatch(uri, "/path/info"); + request.setHttpURI(uri); + request.setContext(_context._apiContext, "/path/info"); ContextHandler.CoreContextRequest coreRequest = response.getHttpChannel().getCoreRequest(); coreRequest.setSessionManager(sessionHandler.getSessionManager()); @@ -1786,7 +1789,8 @@ public class ResponseTest if (host != null) uri.authority(host, port); uri.pathQuery("/path/info;param;jsessionid=12345?query=0&more=1#target"); - request.onDispatch(uri, "/info"); + request.setHttpURI(uri); + request.setContext(_context._apiContext, "/info"); ContextHandler.CoreContextRequest coreRequest = response.getHttpChannel().getCoreRequest(); coreRequest.setRequestedSession(new AbstractSessionManager.RequestedSession(null, "12345", i > 2)); @@ -2268,6 +2272,7 @@ public class ResponseTest org.eclipse.jetty.server.Response coreResponse = new MockResponse(coreRequest); _channel.onRequest(new ContextHandler.CoreContextRequest(coreRequest, _context.getCoreContextHandler().getContext(), _channel)); + _channel.getRequest().setContext(_context._apiContext, URIUtil.decodePath(org.eclipse.jetty.server.Request.getPathInContext(coreRequest))); _channel.onProcess(coreResponse, Callback.NOOP); BufferUtil.clear(_content); From a2832b8ec103c4d377050e41f79eff9a772c2bd4 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Mon, 19 Jun 2023 17:19:10 +1000 Subject: [PATCH 09/63] Issue #9919 - Avoid creating two ByteBufferPools for the server Signed-off-by: Lachlan Roberts --- .../src/main/config/etc/jetty.xml | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty.xml b/jetty-core/jetty-server/src/main/config/etc/jetty.xml index a249cbc5de9..cb797fea938 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty.xml @@ -24,24 +24,15 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + From 5d87ea7254b7cc3830f6c2fed257b970ac961f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Nie=C3=9Fing?= <36885591+zUniQueX@users.noreply.github.com> Date: Tue, 20 Jun 2023 02:54:16 +0200 Subject: [PATCH 10/63] Fix spotbugs packages (#9929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix spotbugs packages Signed-off-by: Steffen Nießing * remove jdk17 profile which disable spotbugs --------- Signed-off-by: Steffen Nießing Co-authored-by: Olivier Lamy --- jetty-core/jetty-alpn/jetty-alpn-client/pom.xml | 2 +- jetty-core/jetty-alpn/jetty-alpn-server/pom.xml | 2 +- jetty-core/jetty-client/pom.xml | 2 +- jetty-core/jetty-deploy/pom.xml | 2 +- jetty-ee10/jetty-ee10-annotations/pom.xml | 2 +- jetty-ee10/jetty-ee10-jaspi/pom.xml | 2 +- jetty-ee10/jetty-ee10-plus/pom.xml | 2 +- jetty-ee10/jetty-ee10-servlet/pom.xml | 2 +- jetty-ee8/jetty-ee8-annotations/pom.xml | 2 +- jetty-ee8/jetty-ee8-nested/pom.xml | 2 +- jetty-ee8/jetty-ee8-openid/pom.xml | 2 +- jetty-ee8/jetty-ee8-plus/pom.xml | 2 +- jetty-ee8/jetty-ee8-security/pom.xml | 2 +- jetty-ee8/jetty-ee8-servlet/pom.xml | 2 +- jetty-ee9/jetty-ee9-annotations/pom.xml | 2 +- jetty-ee9/jetty-ee9-jaspi/pom.xml | 2 +- jetty-ee9/jetty-ee9-nested/pom.xml | 2 +- jetty-ee9/jetty-ee9-openid/pom.xml | 2 +- jetty-ee9/jetty-ee9-plus/pom.xml | 2 +- jetty-ee9/jetty-ee9-proxy/pom.xml | 2 +- jetty-ee9/jetty-ee9-security/pom.xml | 2 +- jetty-ee9/jetty-ee9-servlet/pom.xml | 2 +- pom.xml | 11 ----------- 23 files changed, 22 insertions(+), 33 deletions(-) diff --git a/jetty-core/jetty-alpn/jetty-alpn-client/pom.xml b/jetty-core/jetty-alpn/jetty-alpn-client/pom.xml index 15fe0581305..6d704c1d41e 100644 --- a/jetty-core/jetty-alpn/jetty-alpn-client/pom.xml +++ b/jetty-core/jetty-alpn/jetty-alpn-client/pom.xml @@ -9,7 +9,7 @@ Core :: ALPN :: Client ${project.groupId}.alpn.client - org.eclipse.alpn.* + org.eclipse.jetty.alpn.* diff --git a/jetty-core/jetty-alpn/jetty-alpn-server/pom.xml b/jetty-core/jetty-alpn/jetty-alpn-server/pom.xml index 2d753a0ffb3..f1b437678c8 100644 --- a/jetty-core/jetty-alpn/jetty-alpn-server/pom.xml +++ b/jetty-core/jetty-alpn/jetty-alpn-server/pom.xml @@ -9,7 +9,7 @@ Core :: ALPN :: Server ${project.groupId}.alpn.server - org.eclipse.alpn.* + org.eclipse.jetty.alpn.* diff --git a/jetty-core/jetty-client/pom.xml b/jetty-core/jetty-client/pom.xml index a7a22380ca3..65916cd1016 100644 --- a/jetty-core/jetty-client/pom.xml +++ b/jetty-core/jetty-client/pom.xml @@ -11,7 +11,7 @@ ${project.groupId}.client target/test-policy - org.eclipse.client.* + org.eclipse.jetty.client.* diff --git a/jetty-core/jetty-deploy/pom.xml b/jetty-core/jetty-deploy/pom.xml index 976b1fc413b..2f142de6d8b 100644 --- a/jetty-core/jetty-deploy/pom.xml +++ b/jetty-core/jetty-deploy/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.deploy - org.eclipse.deploy.* + org.eclipse.jetty.deploy.* diff --git a/jetty-ee10/jetty-ee10-annotations/pom.xml b/jetty-ee10/jetty-ee10-annotations/pom.xml index 55c5bd4fb5c..7a79741986c 100644 --- a/jetty-ee10/jetty-ee10-annotations/pom.xml +++ b/jetty-ee10/jetty-ee10-annotations/pom.xml @@ -11,7 +11,7 @@ Annotation support for deploying servlets in jetty. ${project.groupId}.annotations - org.eclipse.annotations.* + org.eclipse.jetty.ee10.annotations.* diff --git a/jetty-ee10/jetty-ee10-jaspi/pom.xml b/jetty-ee10/jetty-ee10-jaspi/pom.xml index bbc24c6d851..f6657b54db1 100644 --- a/jetty-ee10/jetty-ee10-jaspi/pom.xml +++ b/jetty-ee10/jetty-ee10-jaspi/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.security.jaspi - org.eclipse.jetty.jaspi.* + org.eclipse.jetty.ee10.security.jaspi.* diff --git a/jetty-ee10/jetty-ee10-plus/pom.xml b/jetty-ee10/jetty-ee10-plus/pom.xml index 2c0f5384568..6c10c5426aa 100644 --- a/jetty-ee10/jetty-ee10-plus/pom.xml +++ b/jetty-ee10/jetty-ee10-plus/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.plus - org.eclipse.jetty.plus.* + org.eclipse.jetty.ee10.plus.* diff --git a/jetty-ee10/jetty-ee10-servlet/pom.xml b/jetty-ee10/jetty-ee10-servlet/pom.xml index c7a9aae1122..7d1d56287e6 100644 --- a/jetty-ee10/jetty-ee10-servlet/pom.xml +++ b/jetty-ee10/jetty-ee10-servlet/pom.xml @@ -13,7 +13,7 @@ ${project.groupId}.servlet - org.eclipse.jetty.* + org.eclipse.jetty.ee10.servlet.* diff --git a/jetty-ee8/jetty-ee8-annotations/pom.xml b/jetty-ee8/jetty-ee8-annotations/pom.xml index cb1fc57f202..c506c388efb 100644 --- a/jetty-ee8/jetty-ee8-annotations/pom.xml +++ b/jetty-ee8/jetty-ee8-annotations/pom.xml @@ -12,7 +12,7 @@ jetty-ee9-annotations ${project.groupId}.annotations - org.eclipse.annotations.* + org.eclipse.jetty.ee8.annotations.* diff --git a/jetty-ee8/jetty-ee8-nested/pom.xml b/jetty-ee8/jetty-ee8-nested/pom.xml index 5c0da52caef..3c22450b00c 100644 --- a/jetty-ee8/jetty-ee8-nested/pom.xml +++ b/jetty-ee8/jetty-ee8-nested/pom.xml @@ -13,7 +13,7 @@ jetty-ee9-nested ${project.groupId}.server - org.eclipse.jetty.server.* + org.eclipse.jetty.ee8.server.* diff --git a/jetty-ee8/jetty-ee8-openid/pom.xml b/jetty-ee8/jetty-ee8-openid/pom.xml index 7940f790f40..0e654195038 100644 --- a/jetty-ee8/jetty-ee8-openid/pom.xml +++ b/jetty-ee8/jetty-ee8-openid/pom.xml @@ -13,7 +13,7 @@ jetty-ee9-openid ${project.groupId}.openid - org.eclipse.jetty.security.openid.* + org.eclipse.jetty.ee8.security.openid.* diff --git a/jetty-ee8/jetty-ee8-plus/pom.xml b/jetty-ee8/jetty-ee8-plus/pom.xml index d2d026518d8..0ca7bdc7faf 100644 --- a/jetty-ee8/jetty-ee8-plus/pom.xml +++ b/jetty-ee8/jetty-ee8-plus/pom.xml @@ -13,7 +13,7 @@ jetty-ee9-plus ${project.groupId}.plus - org.eclipse.jetty.plus.* + org.eclipse.jetty.ee8.plus.* diff --git a/jetty-ee8/jetty-ee8-security/pom.xml b/jetty-ee8/jetty-ee8-security/pom.xml index 8e98c7c8ba8..c13962d9493 100644 --- a/jetty-ee8/jetty-ee8-security/pom.xml +++ b/jetty-ee8/jetty-ee8-security/pom.xml @@ -13,7 +13,7 @@ jetty-ee9-security ${project.groupId}.security - org.eclipse.jetty.security.* + org.eclipse.jetty.ee8.security.* diff --git a/jetty-ee8/jetty-ee8-servlet/pom.xml b/jetty-ee8/jetty-ee8-servlet/pom.xml index af9f63d4418..767a666dd28 100644 --- a/jetty-ee8/jetty-ee8-servlet/pom.xml +++ b/jetty-ee8/jetty-ee8-servlet/pom.xml @@ -14,7 +14,7 @@ jetty-ee9-servlet ${project.groupId}.servlet - org.eclipse.jetty.* + org.eclipse.jetty.ee8.servlet.* diff --git a/jetty-ee9/jetty-ee9-annotations/pom.xml b/jetty-ee9/jetty-ee9-annotations/pom.xml index d2b2b435d2d..45ebb476faf 100644 --- a/jetty-ee9/jetty-ee9-annotations/pom.xml +++ b/jetty-ee9/jetty-ee9-annotations/pom.xml @@ -11,7 +11,7 @@ Annotation support for deploying servlets in jetty. ${project.groupId}.annotations - org.eclipse.annotations.* + org.eclipse.jetty.ee9.annotations.* diff --git a/jetty-ee9/jetty-ee9-jaspi/pom.xml b/jetty-ee9/jetty-ee9-jaspi/pom.xml index 4fc3d1ccc61..a72509eca14 100644 --- a/jetty-ee9/jetty-ee9-jaspi/pom.xml +++ b/jetty-ee9/jetty-ee9-jaspi/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.security.jaspi - org.eclipse.jetty.jaspi.* + org.eclipse.jetty.ee9.security.jaspi.* diff --git a/jetty-ee9/jetty-ee9-nested/pom.xml b/jetty-ee9/jetty-ee9-nested/pom.xml index 4c30208cee4..bbe41748ad6 100644 --- a/jetty-ee9/jetty-ee9-nested/pom.xml +++ b/jetty-ee9/jetty-ee9-nested/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.server - org.eclipse.jetty.server.* + org.eclipse.jetty.ee9.nested.* diff --git a/jetty-ee9/jetty-ee9-openid/pom.xml b/jetty-ee9/jetty-ee9-openid/pom.xml index 006fc9bef8f..8a19ee7ec03 100644 --- a/jetty-ee9/jetty-ee9-openid/pom.xml +++ b/jetty-ee9/jetty-ee9-openid/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.openid - org.eclipse.jetty.security.openid.* + org.eclipse.jetty.ee9.security.openid.* diff --git a/jetty-ee9/jetty-ee9-plus/pom.xml b/jetty-ee9/jetty-ee9-plus/pom.xml index 28d7ff98d48..21d5306aafd 100644 --- a/jetty-ee9/jetty-ee9-plus/pom.xml +++ b/jetty-ee9/jetty-ee9-plus/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.plus - org.eclipse.jetty.plus.* + org.eclipse.jetty.ee9.plus.* diff --git a/jetty-ee9/jetty-ee9-proxy/pom.xml b/jetty-ee9/jetty-ee9-proxy/pom.xml index fd643c0154f..d31b02db568 100644 --- a/jetty-ee9/jetty-ee9-proxy/pom.xml +++ b/jetty-ee9/jetty-ee9-proxy/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.proxy - org.eclipse.jetty.proxy.* + org.eclipse.jetty.ee9.proxy.* diff --git a/jetty-ee9/jetty-ee9-security/pom.xml b/jetty-ee9/jetty-ee9-security/pom.xml index 1cf8dedb965..efa9a133d11 100644 --- a/jetty-ee9/jetty-ee9-security/pom.xml +++ b/jetty-ee9/jetty-ee9-security/pom.xml @@ -12,7 +12,7 @@ ${project.groupId}.security - org.eclipse.jetty.security.* + org.eclipse.jetty.ee9.security.* diff --git a/jetty-ee9/jetty-ee9-servlet/pom.xml b/jetty-ee9/jetty-ee9-servlet/pom.xml index 8a748af8690..08d2a61fb76 100644 --- a/jetty-ee9/jetty-ee9-servlet/pom.xml +++ b/jetty-ee9/jetty-ee9-servlet/pom.xml @@ -13,7 +13,7 @@ ${project.groupId}.servlet - org.eclipse.jetty.* + org.eclipse.jetty.ee9.servlet.* diff --git a/pom.xml b/pom.xml index d14253ea24d..9d75d3a2347 100644 --- a/pom.xml +++ b/pom.xml @@ -2099,17 +2099,6 @@ - - jdk17 - - [17,) - - - - true - true - - update-version From c6f19a21eb28abe6093f324b6f3953d75fc9e0c9 Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Tue, 20 Jun 2023 12:06:29 +1000 Subject: [PATCH 11/63] Upgrade of dependencies: slf4j 2.0.7, and some 3rd parties dependencies used for testing (#9924) * Upgrade of dependencies: slf4j 2.0.7, and some 3rd parties dependencies used for testing Signed-off-by: Olivier Lamy * update jackson to 2.14.3 Signed-off-by: Olivier Lamy --------- Signed-off-by: Olivier Lamy --- jetty-ee10/pom.xml | 6 +++--- pom.xml | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/jetty-ee10/pom.xml b/jetty-ee10/pom.xml index e5e42b7d929..f13d9f9b824 100644 --- a/jetty-ee10/pom.xml +++ b/jetty-ee10/pom.xml @@ -13,7 +13,7 @@ pom - 2.1.1 + 2.1.2 2.1.1 3.0.0 5.0.1 @@ -21,7 +21,7 @@ 4.0.1 2.0.1 2.1.0 - 2.1.1 + 2.1.2 2.0.1 6.0.0 3.1.1 @@ -36,7 +36,7 @@ 10.1.7 2.0.1 - 5.1.0.Final + 5.1.1.Final true diff --git a/pom.xml b/pom.xml index 9d75d3a2347..9da477b1cf6 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 2.2.1 9.5 4.2.0 - 6.4.0 + 6.4.1 1.5 10.6.0 1.15 @@ -58,10 +58,10 @@ 32.0.1-jre 5.1.0 2.2 - 5.2.2 + 5.3.1 4.6.2.Final 11.0.17.Final - 2.14.2 + 2.14.3 2.0.1 2.1.1 4.0.0 @@ -85,7 +85,7 @@ 1.4.1.v201005082020 2.2.1.Final 2.2.1.Final - 3.5.0.Final + 3.5.1.Final 2.3.0.Alpha1 3.5.0.Final 1.1 @@ -103,7 +103,7 @@ 2.0.3 2.20.0 1.4.7 - 3.1.2 + 3.1.4 10.3.6 3.8.7 0.13.1 @@ -116,13 +116,13 @@ 1.2.0 1.3.0 - 3.18.200 + 3.18.400 3.11.100 1.6.1 1.5.1 1.4.1 - 3.7.100 + 3.7.200 1.2.0 1.0.2 1.0.1 @@ -137,7 +137,7 @@ 2.1.1 3.5.1 - 2.0.6 + 2.0.7 1.3.6 2.1.1.RELEASE 1.2.5 From 87eb651c1f84d8f95c4d27524d2925a3bdceee33 Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Tue, 20 Jun 2023 13:52:26 +1000 Subject: [PATCH 12/63] mark CloseableDoSFilterTest#testEvenLowRateIP as flaky tests (#9930) Signed-off-by: Olivier Lamy --- .../jetty/ee10/servlets/CloseableDoSFilterTest.java | 10 ++++++++++ .../jetty/ee9/servlets/CloseableDoSFilterTest.java | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CloseableDoSFilterTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CloseableDoSFilterTest.java index 72baeb38e53..8be6890bd00 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CloseableDoSFilterTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CloseableDoSFilterTest.java @@ -16,6 +16,8 @@ package org.eclipse.jetty.ee10.servlets; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(WorkDirExtension.class) @@ -28,4 +30,12 @@ public class CloseableDoSFilterTest extends AbstractDoSFilterTest { startServer(workDir.getEmptyPathDir(), CloseableDoSFilter.class); } + + @Override + @Test + @Tag("flaky") + public void testEvenLowRateIP() throws Exception + { + super.testEvenLowRateIP(); + } } diff --git a/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java b/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java index dbfd4af3f63..03131097deb 100644 --- a/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java +++ b/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java @@ -38,4 +38,13 @@ public class CloseableDoSFilterTest extends AbstractDoSFilterTest { super.testUnavailableIP(); } + + @Override + @Test + @Tag("flaky") + public void testEvenLowRateIP() throws Exception + { + super.testEvenLowRateIP(); + } + } From bf5e0e94f8a0219bf72d00f56139e0fc96755aa7 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 20 Jun 2023 20:52:43 +1000 Subject: [PATCH 13/63] Maintain context path in original nested Requests pathInContext Signed-off-by: Lachlan Roberts --- .../src/main/java/org/eclipse/jetty/ee9/nested/Request.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b32599aedd2..d0799b1e8b9 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 @@ -1501,11 +1501,11 @@ public class Request implements HttpServletRequest _method = coreRequest.getMethod(); _uri = coreRequest.getHttpURI(); - - String pathInContext = org.eclipse.jetty.server.Request.getPathInContext(coreRequest); - _pathInContext = URIUtil.decodePath(pathInContext); _httpFields = coreRequest.getHeaders(); + // This is further modified inside ContextHandler.doScope(). + _pathInContext = coreRequest.getHttpURI().getCanonicalPath(); + setSecure(coreRequest.isSecure()); } From a5f26b3d1fe46d719704e9f753584853e95cceea Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Tue, 20 Jun 2023 16:16:59 +0200 Subject: [PATCH 14/63] Issue #9925 Use current ServletContextHandler (#9933) --- .../org/eclipse/jetty/ee10/servlet/ServletContextHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java index 751b963286d..cf2fba2d306 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java @@ -164,7 +164,8 @@ public class ServletContextHandler extends ContextHandler { if (servletContext instanceof ServletContextApi) return ((ServletContextApi)servletContext).getContext().getServletContextHandler(); - return null; + + return getCurrentServletContextHandler(); } public static ServletContext getCurrentServletContext() From 5d971756866732e1af75ffafbea57ffab6e544cf Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Tue, 20 Jun 2023 17:27:15 +0200 Subject: [PATCH 15/63] Remove ee10 HttpChannel.Listener (#9920) * remove ee10 HttpChannel.Listener Signed-off-by: Ludovic Orban --- .../jetty/server/handler/EventsHandler.java | 2 +- .../jetty/ee10/servlet/ServletChannel.java | 352 +----------------- .../jetty/ee10/servlet/AsyncServletTest.java | 19 +- .../ee10/servlet/ServletRequestLogTest.java | 97 +++-- .../ee10/test/GzipWithSendErrorTest.java | 70 ++-- .../tests/WebSocketOverHTTP2Test.java | 24 +- 6 files changed, 115 insertions(+), 449 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java index ab71cfa1529..4a3932be43f 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java @@ -160,7 +160,7 @@ public abstract class EventsHandler extends Handler.Wrapper { try { - onResponseWrite(request, last, content.asReadOnlyBuffer()); + onResponseWrite(request, last, content == null ? null : content.asReadOnlyBuffer()); } catch (Throwable x) { diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java index 586994a8485..6ced9cd2d8b 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java @@ -16,16 +16,9 @@ package org.eclipse.jetty.ee10.servlet; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.EventListener; -import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import jakarta.servlet.RequestDispatcher; import org.eclipse.jetty.ee10.servlet.ServletRequestState.Action; @@ -80,7 +73,6 @@ public class ServletChannel private final HttpConfiguration _configuration; private final EndPoint _endPoint; private final HttpInput _httpInput; - private final Listener _combinedListener; private volatile ServletContextRequest _servletContextRequest; private volatile boolean _expects100Continue; private volatile Callback _callback; @@ -97,7 +89,6 @@ public class ServletChannel _configuration = request.getConnectionMetaData().getHttpConfiguration(); _endPoint = request.getConnectionMetaData().getConnection().getEndPoint(); _httpInput = new HttpInput(this); - _combinedListener = new Listeners(_connector, servletContextHandler); } public void setCallback(Callback callback) @@ -676,22 +667,9 @@ public class ServletChannel private void dispatch(Dispatchable dispatchable) throws Exception { - try - { - _servletContextRequest.getResponse().getHttpOutput().reopen(); - getHttpOutput().reopen(); - _combinedListener.onBeforeDispatch(_servletContextRequest); - dispatchable.dispatch(); - } - catch (Throwable x) - { - _combinedListener.onDispatchFailure(_servletContextRequest, x); - throw x; - } - finally - { - _combinedListener.onAfterDispatch(_servletContextRequest); - } + _servletContextRequest.getResponse().getHttpOutput().reopen(); + getHttpOutput().reopen(); + dispatchable.dispatch(); } /** @@ -824,7 +802,6 @@ public class ServletChannel void onTrailers(HttpFields trailers) { _servletContextRequest.setTrailers(trailers); - _combinedListener.onRequestTrailers(_servletContextRequest); } /** @@ -851,10 +828,7 @@ public class ServletChannel // Recycle always done here even if an abort is called. recycle(); if (_state.completeResponse()) - { - _combinedListener.onComplete(servletContextRequest); callback.succeeded(); - } } public boolean isCommitted() @@ -897,9 +871,7 @@ public class ServletChannel { if (LOG.isDebugEnabled()) LOG.debug("abort {}", this, failure); - Callback callback = _callback; - _combinedListener.onResponseFailure(_servletContextRequest, failure); - callback.failed(failure); + _callback.failed(failure); } } @@ -907,320 +879,4 @@ public class ServletChannel { void dispatch() throws Exception; } - - /** - *

Listener for Channel events.

- *

HttpChannel will emit events for the various phases it goes through while - * processing an HTTP request and response.

- *

Implementations of this interface may listen to those events to track - * timing and/or other values such as request URI, etc.

- *

The events parameters, especially the {@link Request} object, may be - * in a transient state depending on the event, and not all properties/features - * of the parameters may be available inside a listener method.

- *

It is recommended that the event parameters are not acted upon - * in the listener methods, or undefined behavior may result. For example, it - * would be a bad idea to try to read some content from the - * {@link jakarta.servlet.ServletInputStream} in listener methods. On the other - * hand, it is legit to store request attributes in one listener method that - * may be possibly retrieved in another listener method in a later event.

- *

Listener methods are invoked synchronously from the thread that is - * performing the request processing, and they should not call blocking code - * (otherwise the request processing will be blocked as well).

- *

Listener instances that are set as a bean on the {@link Connector} are - * also added. If additional listeners are added - * using the deprecated {@code HttpChannel#addListener(Listener)}

method, - * then an instance of {@code TransientListeners} must be added to the connector - * in order for them to be invoked. - */ - // TODO: looks like a lot of these methods are never called. - public interface Listener extends EventListener - { - /** - * Invoked just after the HTTP request line and headers have been parsed. - * - * @param request the request object - */ - default void onRequestBegin(Request request) - { - } - - /** - * Invoked just before calling the application. - * - * @param request the request object - */ - default void onBeforeDispatch(Request request) - { - } - - /** - * Invoked when the application threw an exception. - * - * @param request the request object - * @param failure the exception thrown by the application - */ - default void onDispatchFailure(Request request, Throwable failure) - { - } - - /** - * Invoked just after the application returns from the first invocation. - * - * @param request the request object - */ - default void onAfterDispatch(Request request) - { - } - - /** - * Invoked every time a request content chunk has been parsed, just before - * making it available to the application. - * - * @param request the request object - * @param content a {@link ByteBuffer#slice() slice} of the request content chunk - */ - default void onRequestContent(Request request, ByteBuffer content) - { - } - - /** - * Invoked when the end of the request content is detected. - * - * @param request the request object - */ - default void onRequestContentEnd(Request request) - { - } - - /** - * Invoked when the request trailers have been parsed. - * - * @param request the request object - */ - default void onRequestTrailers(Request request) - { - } - - /** - * Invoked when the request has been fully parsed. - * - * @param request the request object - */ - default void onRequestEnd(Request request) - { - } - - /** - * Invoked when the request processing failed. - * - * @param request the request object - * @param failure the request failure - */ - default void onRequestFailure(Request request, Throwable failure) - { - } - - /** - * Invoked just before the response line is written to the network. - * - * @param request the request object - */ - default void onResponseBegin(Request request) - { - } - - /** - * Invoked just after the response is committed (that is, the response - * line, headers and possibly some content have been written to the - * network). - * - * @param request the request object - */ - default void onResponseCommit(Request request) - { - } - - /** - * Invoked after a response content chunk has been written to the network. - * - * @param request the request object - * @param content a {@link ByteBuffer#slice() slice} of the response content chunk - */ - default void onResponseContent(Request request, ByteBuffer content) - { - } - - /** - * Invoked when the response has been fully written. - * - * @param request the request object - */ - default void onResponseEnd(Request request) - { - } - - /** - * Invoked when the response processing failed. - * - * @param request the request object - * @param failure the response failure - */ - default void onResponseFailure(Request request, Throwable failure) - { - } - - /** - * Invoked when the request and response processing are complete. - * - * @param request the request object - */ - default void onComplete(Request request) - { - } - } - - private static class Listeners implements Listener - { - private final List _listeners; - - private Listeners(Connector connector, ServletContextHandler servletContextHandler) - { - Collection connectorListeners = connector.getBeans(Listener.class); - List handlerListeners = servletContextHandler.getEventListeners().stream() - .filter(l -> l instanceof Listener) - .map(Listener.class::cast) - .toList(); - _listeners = new ArrayList<>(connectorListeners); - _listeners.addAll(handlerListeners); - } - - @Override - public void onRequestBegin(Request request) - { - _listeners.forEach(l -> notify(l::onRequestBegin, request)); - } - - @Override - public void onBeforeDispatch(Request request) - { - _listeners.forEach(l -> notify(l::onBeforeDispatch, request)); - } - - @Override - public void onDispatchFailure(Request request, Throwable failure) - { - _listeners.forEach(l -> notify(l::onDispatchFailure, request, failure)); - } - - @Override - public void onAfterDispatch(Request request) - { - _listeners.forEach(l -> notify(l::onAfterDispatch, request)); - } - - @Override - public void onRequestContent(Request request, ByteBuffer content) - { - _listeners.forEach(l -> notify(l::onRequestContent, request, content)); - } - - @Override - public void onRequestContentEnd(Request request) - { - _listeners.forEach(l -> notify(l::onRequestContentEnd, request)); - } - - @Override - public void onRequestTrailers(Request request) - { - _listeners.forEach(l -> notify(l::onRequestTrailers, request)); - } - - @Override - public void onRequestEnd(Request request) - { - _listeners.forEach(l -> notify(l::onRequestEnd, request)); - } - - @Override - public void onRequestFailure(Request request, Throwable failure) - { - _listeners.forEach(l -> notify(l::onRequestFailure, request, failure)); - } - - @Override - public void onResponseBegin(Request request) - { - _listeners.forEach(l -> notify(l::onResponseBegin, request)); - } - - @Override - public void onResponseCommit(Request request) - { - _listeners.forEach(l -> notify(l::onResponseCommit, request)); - } - - @Override - public void onResponseContent(Request request, ByteBuffer content) - { - _listeners.forEach(l -> notify(l::onResponseContent, request, content)); - } - - @Override - public void onResponseEnd(Request request) - { - _listeners.forEach(l -> notify(l::onResponseEnd, request)); - } - - @Override - public void onResponseFailure(Request request, Throwable failure) - { - _listeners.forEach(l -> notify(l::onResponseFailure, request, failure)); - } - - @Override - public void onComplete(Request request) - { - _listeners.forEach(l -> notify(l::onComplete, request)); - } - - private void notify(Consumer consumer, Request request) - { - try - { - consumer.accept(request); - } - catch (Throwable x) - { - if (LOG.isDebugEnabled()) - LOG.debug("failure while notifying %s event for %s".formatted(ServletChannel.Listener.class.getSimpleName(), request)); - } - } - - private void notify(BiConsumer consumer, Request request, Throwable failure) - { - try - { - consumer.accept(request, failure); - } - catch (Throwable x) - { - if (LOG.isDebugEnabled()) - LOG.debug("failure while notifying %s event for %s".formatted(ServletChannel.Listener.class.getSimpleName(), request)); - } - } - - private void notify(BiConsumer consumer, Request request, ByteBuffer byteBuffer) - { - try - { - consumer.accept(request, byteBuffer.slice()); - } - catch (Throwable x) - { - if (LOG.isDebugEnabled()) - LOG.debug("failure while notifying %s event for %s".formatted(ServletChannel.Listener.class.getSimpleName(), request)); - } - } - } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java index 854e124382d..bcc9df039dd 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.EventsHandler; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.component.AbstractLifeCycle; @@ -99,7 +100,14 @@ public class AsyncServletTest ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); context.setContextPath("/ctx"); - _server.setHandler(context); + _server.setHandler(new EventsHandler(context) + { + @Override + protected void onComplete(Request request, Throwable failure) + { + _latch.countDown(); + } + }); context.addEventListener(new DebugListener()); _errorHandler = new ErrorPageErrorHandler(); @@ -122,15 +130,6 @@ public class AsyncServletTest _port = _connector.getLocalPort(); _history.clear(); _latch = new CountDownLatch(1); - - context.addEventListener(new ServletChannel.Listener() - { - @Override - public void onComplete(Request request) - { - _latch.countDown(); - } - }); } @AfterEach diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java index 1de2d34258b..2556e497791 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java @@ -41,6 +41,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.DefaultHandler; +import org.eclipse.jetty.server.handler.EventsHandler; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; @@ -297,13 +298,20 @@ public class ServletRequestLogTest // id="DefaultHandler" DefaultHandler defaultHandler = new DefaultHandler(); - server.setHandler(contexts); - // Next the behavior as defined by etc/jetty-requestlog.xml // the id="RequestLog" CaptureLog captureLog = new CaptureLog(); server.setRequestLog(captureLog); + server.setHandler(new EventsHandler(contexts) + { + @Override + protected void onComplete(Request request, Throwable failure) + { + assertRequestLog(expectedLogEntry, captureLog); + } + }); + // Lastly, the behavior as defined by deployment of a webapp // Add the Servlet Context ServletContextHandler app = new ServletContextHandler(ServletContextHandler.SESSIONS); @@ -320,15 +328,6 @@ public class ServletRequestLogTest Assertions.assertTimeoutPreemptively(ofSeconds(4), () -> { - app.addEventListener(new ServletChannel.Listener() - { - @Override - public void onComplete(Request request) - { - assertRequestLog(expectedLogEntry, captureLog); - } - }); - String host = connector.getHost(); if (host == null) host = "localhost"; @@ -372,14 +371,21 @@ public class ServletRequestLogTest ErrorHandler errorHandler = new ErrorHandler(); server.addBean(errorHandler); - ContextHandlerCollection contexts = new ContextHandlerCollection(); - server.setHandler(contexts); - - // Next the behavior as defined by etc/jetty-requestlog.xml + // The behavior as defined by etc/jetty-requestlog.xml // the id="RequestLog" CaptureLog captureLog = new CaptureLog(); server.setRequestLog(captureLog); + ContextHandlerCollection contexts = new ContextHandlerCollection(); + server.setHandler(new EventsHandler(contexts) + { + @Override + protected void onComplete(Request request, Throwable failure) + { + assertRequestLog(expectedLogEntry, captureLog); + } + }); + // Lastly, the behavior as defined by deployment of a webapp // Add the Servlet Context ServletContextHandler app = new ServletContextHandler(ServletContextHandler.SESSIONS); @@ -396,15 +402,6 @@ public class ServletRequestLogTest Assertions.assertTimeoutPreemptively(ofSeconds(4), () -> { - app.addEventListener(new ServletChannel.Listener() - { - @Override - public void onComplete(Request request) - { - assertRequestLog(expectedLogEntry, captureLog); - } - }); - String host = connector.getHost(); if (host == null) host = "localhost"; @@ -445,14 +442,21 @@ public class ServletRequestLogTest connector.setPort(0); server.setConnectors(new Connector[]{connector}); - ContextHandlerCollection contexts = new ContextHandlerCollection(); - server.setHandler(contexts); - - // Next the behavior as defined by etc/jetty-requestlog.xml + // The behavior as defined by etc/jetty-requestlog.xml // the id="RequestLog" CaptureLog captureLog = new CaptureLog(); server.setRequestLog(captureLog); + ContextHandlerCollection contexts = new ContextHandlerCollection(); + server.setHandler(new EventsHandler(contexts) + { + @Override + protected void onComplete(Request request, Throwable failure) + { + assertRequestLog(expectedLogEntry, captureLog); + } + }); + // Lastly, the behavior as defined by deployment of a webapp // Add the Servlet Context ServletContextHandler app = new ServletContextHandler(ServletContextHandler.SESSIONS); @@ -475,15 +479,6 @@ public class ServletRequestLogTest Assertions.assertTimeoutPreemptively(ofSeconds(4), () -> { - app.addEventListener(new ServletChannel.Listener() - { - @Override - public void onComplete(Request request) - { - assertRequestLog(expectedLogEntry, captureLog); - } - }); - String host = connector.getHost(); if (host == null) host = "localhost"; @@ -523,16 +518,23 @@ public class ServletRequestLogTest connector.setPort(0); server.setConnectors(new Connector[]{connector}); - // First the behavior as defined in etc/jetty.xml (as is) - // id="Contexts" - ContextHandlerCollection contexts = new ContextHandlerCollection(); - server.setHandler(contexts); - - // Next the proposed behavioral change to etc/jetty-requestlog.xml + // The proposed behavioral change to etc/jetty-requestlog.xml // the id="RequestLog" CaptureLog captureLog = new CaptureLog(); server.setRequestLog(captureLog); + // The behavior as defined in etc/jetty.xml (as is) + // id="Contexts" + ContextHandlerCollection contexts = new ContextHandlerCollection(); + server.setHandler(new EventsHandler(contexts) + { + @Override + protected void onComplete(Request request, Throwable failure) + { + assertRequestLog(expectedLogEntry, captureLog); + } + }); + // Lastly, the behavior as defined by deployment of a webapp // Add the Servlet Context ServletContextHandler app = new ServletContextHandler(ServletContextHandler.SESSIONS); @@ -555,15 +557,6 @@ public class ServletRequestLogTest Assertions.assertTimeoutPreemptively(ofSeconds(4), () -> { - app.addEventListener(new ServletChannel.Listener() - { - @Override - public void onComplete(Request request) - { - assertRequestLog(expectedLogEntry, captureLog); - } - }); - String host = connector.getHost(); if (host == null) host = "localhost"; diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java index cffd421eb39..c2ba230c2c0 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java @@ -24,9 +24,12 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -36,12 +39,12 @@ import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.Response; import org.eclipse.jetty.ee10.servlet.HttpInput; -import org.eclipse.jetty.ee10.servlet.ServletChannel; +import org.eclipse.jetty.ee10.servlet.ListenerHolder; +import org.eclipse.jetty.ee10.servlet.ServletApiRequest; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.gzip.GzipHandler; @@ -72,6 +75,7 @@ public class GzipWithSendErrorTest private Server server; private HttpClient client; private ServerConnector connector; + private Consumer onComplete; @BeforeEach public void setup() throws Exception @@ -90,6 +94,25 @@ public class GzipWithSendErrorTest contextHandler.addServlet(PostServlet.class, "/submit"); contextHandler.addServlet(FailServlet.class, "/fail"); + ListenerHolder listenerHolder = new ListenerHolder(); + listenerHolder.setListener(new ServletRequestListener() + { + @Override + public void requestDestroyed(ServletRequestEvent sre) + { + if (onComplete != null) + { + ServletContextRequest servletContextRequest = ((ServletApiRequest)sre.getServletRequest()).getServletContextRequest(); + onComplete.accept(servletContextRequest); + } + } + + @Override + public void requestInitialized(ServletRequestEvent sre) + { + } + }); + contextHandler.getServletHandler().addListener(listenerHolder); gzipHandler.setHandler(contextHandler); server.setHandler(gzipHandler); @@ -102,6 +125,7 @@ public class GzipWithSendErrorTest @AfterEach public void teardown() { + onComplete = null; LifeCycle.stop(client); LifeCycle.stop(server); } @@ -181,20 +205,15 @@ public class GzipWithSendErrorTest // count of bytes against API read AtomicLong inputContentConsumed = new AtomicLong(0L); - connector.addBean(new ServletChannel.Listener() + onComplete = servletContextRequest -> { - @Override - public void onComplete(Request baseRequest) - { - ServletContextRequest request = Request.as(baseRequest, ServletContextRequest.class); - HttpConnection connection = (HttpConnection)request.getConnectionMetaData().getConnection(); - HttpInput httpInput = request.getHttpInput(); - inputContentConsumed.set(httpInput.getContentConsumed()); - inputContentReceived.set(httpInput.getContentReceived()); - inputBytesIn.set(connection.getBytesIn()); - serverRequestCompleteLatch.countDown(); - } - }); + HttpInput httpInput = servletContextRequest.getHttpInput(); + HttpConnection connection = (HttpConnection)servletContextRequest.getConnectionMetaData().getConnection(); + inputContentConsumed.set(httpInput.getContentConsumed()); + inputContentReceived.set(httpInput.getContentReceived()); + inputBytesIn.set(connection.getBytesIn()); + serverRequestCompleteLatch.countDown(); + }; // This is a doubly-compressed (with gzip) test resource. // There's no point putting into SCM the full 1MB file, when the @@ -288,20 +307,15 @@ public class GzipWithSendErrorTest // count of bytes against API read AtomicLong inputContentConsumed = new AtomicLong(0L); - connector.addBean(new ServletChannel.Listener() + onComplete = servletContextRequest -> { - @Override - public void onComplete(Request baseRequest) - { - ServletContextRequest request = Request.as(baseRequest, ServletContextRequest.class); - HttpConnection connection = (HttpConnection)request.getConnectionMetaData().getConnection(); - HttpInput httpInput = request.getHttpInput(); - inputContentConsumed.set(httpInput.getContentConsumed()); - inputContentReceived.set(httpInput.getContentReceived()); - inputBytesIn.set(connection.getBytesIn()); - serverRequestCompleteLatch.countDown(); - } - }); + HttpInput httpInput = servletContextRequest.getHttpInput(); + HttpConnection connection = (HttpConnection)servletContextRequest.getConnectionMetaData().getConnection(); + inputContentConsumed.set(httpInput.getContentConsumed()); + inputContentReceived.set(httpInput.getContentReceived()); + inputBytesIn.set(connection.getBytesIn()); + serverRequestCompleteLatch.countDown(); + }; // This is a doubly-compressed (with gzip) test resource. // There's no point putting into SCM the full 1MB file, when the diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java index 0bab0daac55..abb5430ed31 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java @@ -31,7 +31,6 @@ import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.transport.HttpClientConnectionFactory; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; -import org.eclipse.jetty.ee10.servlet.ServletChannel; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; @@ -57,6 +56,7 @@ import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.EventsHandler; import org.eclipse.jetty.server.internal.HttpChannelState; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -88,6 +88,7 @@ public class WebSocketOverHTTP2Test private ServerConnector tlsConnector; private WebSocketClient wsClient; private ServletContextHandler context; + private Runnable onComplete; private void startServer() throws Exception { @@ -124,6 +125,16 @@ public class WebSocketOverHTTP2Test context.addServlet(new ServletHolder(servlet), "/ws/*"); JettyWebSocketServletContainerInitializer.configure(context, null); + server.setHandler(new EventsHandler(server.getHandler()) + { + @Override + protected void onComplete(Request request, Throwable failure) + { + if (onComplete != null) + onComplete.run(); + } + }); + server.start(); } @@ -142,6 +153,7 @@ public class WebSocketOverHTTP2Test @AfterEach public void stopServer() throws Exception { + onComplete = null; if (server != null) server.stop(); if (wsClient != null) @@ -302,15 +314,7 @@ public class WebSocketOverHTTP2Test startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); CountDownLatch latch = new CountDownLatch(1); - connector.addBean(new ServletChannel.Listener() - { - @Override - public void onComplete(Request request) - { - latch.countDown(); - } - }); - + onComplete = latch::countDown; EventSocket wsEndPoint = new EventSocket(); URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/throw"); From 8550493c31e32fedc07ee3eb1f063f7b208fcdaa Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 21 Jun 2023 14:58:09 +1000 Subject: [PATCH 16/63] decode Path in content for nested Request Signed-off-by: Lachlan Roberts --- .../src/main/java/org/eclipse/jetty/ee9/nested/Request.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d0799b1e8b9..e5f8b3e7644 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 @@ -1504,7 +1504,7 @@ public class Request implements HttpServletRequest _httpFields = coreRequest.getHeaders(); // This is further modified inside ContextHandler.doScope(). - _pathInContext = coreRequest.getHttpURI().getCanonicalPath(); + _pathInContext = URIUtil.decodePath(coreRequest.getHttpURI().getCanonicalPath()); setSecure(coreRequest.isSecure()); } From 57a863088b8bb8cd679794446288271b14a8c0e5 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 21 Jun 2023 17:06:18 +1000 Subject: [PATCH 17/63] save some attributes for request log and error dispatch Signed-off-by: Lachlan Roberts --- .../org/eclipse/jetty/ee9/nested/ContextHandler.java | 7 +++++++ .../org/eclipse/jetty/ee9/nested/ErrorHandler.java | 5 ++++- .../org/eclipse/jetty/ee9/nested/HttpChannel.java | 12 ++++++++++-- .../eclipse/jetty/ee9/nested/HttpChannelState.java | 4 +++- .../java/org/eclipse/jetty/ee9/nested/Request.java | 9 --------- .../eclipse/jetty/ee9/servlet/ServletHandler.java | 3 +++ .../ee9/servlet/ContextHandlerClassLoaderTest.java | 10 +++++----- 7 files changed, 32 insertions(+), 18 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 c270128d6cb..b45ea51b149 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,6 +97,9 @@ import org.eclipse.jetty.util.security.SecurityUtils; 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. * @@ -902,6 +905,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie (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 6a1aa156f45..141fc78fce5 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 @@ -28,6 +28,7 @@ import java.util.Map; import java.util.stream.Collectors; import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -44,6 +45,8 @@ import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.eclipse.jetty.ee9.nested.HttpChannel.SERVLET_CONTEXT_ATTRIBUTE; + /** * Handler for Error pages */ @@ -88,7 +91,7 @@ public class ErrorHandler extends AbstractHandler // 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; - ContextHandler.APIContext context = baseRequest.getErrorContext(); + ServletContext context = (ServletContext)request.getAttribute(SERVLET_CONTEXT_ATTRIBUTE); 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 b520672ec42..38ed7c9144f 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,6 +32,7 @@ import java.util.stream.Stream; 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; @@ -72,6 +73,9 @@ import static org.eclipse.jetty.util.thread.Invocable.InvocationType.NON_BLOCKIN 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; @@ -1010,11 +1014,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor 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( - _request.getServletName(), - _request.getServletContext().getRealPath(_request.getPathInContext()) + servletName, + servletContext.getRealPath(pathInContext) ); _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 94fd4086e37..75c57d36dca 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,6 +35,7 @@ import static jakarta.servlet.RequestDispatcher.ERROR_MESSAGE; 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. @@ -922,7 +923,8 @@ public class HttpChannelState response.setStatus(code); response.errorClose(); - request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext()); + ServletContext errorContext = (ServletContext)request.getAttribute(SERVLET_CONTEXT_ATTRIBUTE); + request.setAttribute(ErrorHandler.ERROR_CONTEXT, errorContext); request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); request.setAttribute(ERROR_STATUS_CODE, code); 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 e5f8b3e7644..294f9a655de 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 @@ -702,15 +702,6 @@ public class Request implements HttpServletRequest _pathInContext = pathInContext; } - /** - * @return The current {@link ContextHandler.APIContext context} used for this error handling for this request. If the request is asynchronous, - * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext - */ - public ContextHandler.APIContext getErrorContext() - { - return _context; - } - @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 6c441a021a0..6ee23353b9f 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,6 +70,8 @@ import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.eclipse.jetty.ee9.nested.HttpChannel.SERVLET_NAME_ATTRIBUTE; + /** * Servlet HttpHandler. *

@@ -470,6 +472,7 @@ public class ServletHandler extends ScopedHandler if (servletPathMapping != null) { baseRequest.setServletPathMapping(servletPathMapping); + baseRequest.setAttribute(SERVLET_NAME_ATTRIBUTE, servletPathMapping.getServletName()); } } diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java index 6348a22558e..7e23a3694bf 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ContextHandlerClassLoaderTest.java @@ -14,8 +14,7 @@ package org.eclipse.jetty.ee9.servlet; import java.io.IOException; -import java.util.ArrayDeque; -import java.util.Queue; +import java.util.concurrent.TimeUnit; import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncEvent; @@ -30,6 +29,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.ee9.nested.ContextHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BlockingArrayQueue; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -44,7 +44,7 @@ public class ContextHandlerClassLoaderTest private ServerConnector _connector; private Server _server; private HttpClient _client; - private final Queue _asyncListenerLog = new ArrayDeque<>(); + private final BlockingArrayQueue _asyncListenerLog = new BlockingArrayQueue<>(); public static class MyCustomClassLoader extends ClassLoader { @@ -139,7 +139,7 @@ public class ContextHandlerClassLoaderTest assertThat(responseContent, containsString("MyCustomClassLoader")); // AsyncListener should also have the correct ClassLoader set. - assertThat(_asyncListenerLog.poll(), equalTo("onComplete(): MyCustomClassLoader")); + assertThat(_asyncListenerLog.poll(5, TimeUnit.SECONDS), equalTo("onComplete(): MyCustomClassLoader")); assertThat(_asyncListenerLog.size(), equalTo(0)); } @@ -159,7 +159,7 @@ public class ContextHandlerClassLoaderTest assertThat(responseContent, containsString("MyCustomClassLoader")); // AsyncListener should also have the correct ClassLoader set. - assertThat(_asyncListenerLog.poll(), equalTo("onComplete(): MyCustomClassLoader")); + assertThat(_asyncListenerLog.poll(5, TimeUnit.SECONDS), equalTo("onComplete(): MyCustomClassLoader")); assertThat(_asyncListenerLog.size(), equalTo(0)); } } From 9041527f94e3d5456f3f2439c689bf891a805c2f Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 21 Jun 2023 11:18:43 +0200 Subject: [PATCH 18/63] Fix #9476 IteratingCallback multiple onCompleteFailure calls (#9940) Change state to FAILED from PENDING state --- .../eclipse/jetty/util/IteratingCallback.java | 1 + .../jetty/util/IteratingCallbackTest.java | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java index 4ba8bc67753..634dc52a033 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java @@ -400,6 +400,7 @@ public abstract class IteratingCallback implements Callback break; case PENDING: { + _state = State.FAILED; failure = true; break; } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java index 668d9bf1f9d..d588ab518bc 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java @@ -15,6 +15,7 @@ package org.eclipse.jetty.util; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; import org.eclipse.jetty.util.thread.Scheduler; @@ -322,4 +323,44 @@ public class IteratingCallbackTest return isSucceeded(); } } + + @Test + public void testMultipleFailures() throws Exception + { + AtomicInteger process = new AtomicInteger(); + AtomicInteger failure = new AtomicInteger(); + IteratingCallback icb = new IteratingCallback() + { + @Override + protected Action process() throws Throwable + { + process.incrementAndGet(); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteFailure(Throwable cause) + { + super.onCompleteFailure(cause); + failure.incrementAndGet(); + } + }; + + icb.iterate(); + assertEquals(1, process.get()); + assertEquals(0, failure.get()); + + icb.failed(new Throwable("test1")); + + assertEquals(1, process.get()); + assertEquals(1, failure.get()); + + icb.succeeded(); + assertEquals(1, process.get()); + assertEquals(1, failure.get()); + + icb.failed(new Throwable("test2")); + assertEquals(1, process.get()); + assertEquals(1, failure.get()); + } } From b9ca6d22337b64d98563862d47eba3dfbef82f8a Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Wed, 21 Jun 2023 17:50:33 +0200 Subject: [PATCH 19/63] Fix ee10 path info only (alternative) (#9934) Fix ee10 path info only Signed-off-by: Ludovic Orban Co-authored-by: Jan Bartel --- .../eclipse/jetty/server/ResourceService.java | 17 +++--- .../jetty/server/handler/ResourceHandler.java | 4 +- .../jetty/ee10/servlet/DefaultServlet.java | 18 ++---- .../ee10/servlet/DefaultServletTest.java | 57 +++++++++++++++++++ .../src/test/resources/pathTest/default/dot | 0 .../src/test/resources/pathTest/rdir/dot | 0 6 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 jetty-ee10/jetty-ee10-servlet/src/test/resources/pathTest/default/dot create mode 100644 jetty-ee10/jetty-ee10-servlet/src/test/resources/pathTest/rdir/dot 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 b7a4a4422f9..bdd623f7829 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 @@ -440,11 +440,12 @@ public class ResourceService protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, Request request, Response response, Callback callback) throws Exception { + if (!Objects.requireNonNull(content).getResource().isDirectory()) + throw new IllegalArgumentException("content must be a directory"); + if (LOG.isDebugEnabled()) - { LOG.debug("sendWelcome(content={}, pathInContext={}, endsWithSlash={}, req={}, resp={}, callback={})", content, pathInContext, endsWithSlash, request, response, callback); - } // Redirect to directory if (!endsWithSlash) @@ -460,7 +461,7 @@ public class ResourceService } // process optional Welcome behaviors - if (welcome(request, response, callback)) + if (welcome(content, request, response, callback)) return; if (!passConditionalHeaders(request, response, content, callback)) @@ -499,9 +500,9 @@ public class ResourceService { } - private boolean welcome(Request request, Response response, Callback callback) throws Exception + private boolean welcome(HttpContent content, Request request, Response response, Callback callback) throws Exception { - WelcomeAction welcomeAction = processWelcome(request); + WelcomeAction welcomeAction = processWelcome(content, request); if (LOG.isDebugEnabled()) LOG.debug("welcome(req={}, rsp={}, cbk={}) welcomeAction={}", request, response, callback, welcomeAction); @@ -581,9 +582,9 @@ public class ResourceService Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500); } - private WelcomeAction processWelcome(Request request) throws IOException + private WelcomeAction processWelcome(HttpContent content, Request request) throws IOException { - String welcomeTarget = getWelcomeFactory().getWelcomeTarget(request); + String welcomeTarget = getWelcomeFactory().getWelcomeTarget(content, request); if (welcomeTarget == null) return null; @@ -892,7 +893,7 @@ public class ResourceService * @return The URI path of the matching welcome target in context or null * if no welcome target was found */ - String getWelcomeTarget(Request request) throws IOException; + String getWelcomeTarget(HttpContent content, Request request) throws IOException; } private static class ContentWriterIteratingCallback extends IteratingCallback diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java index 675a7eb8aa0..43dea077632 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java @@ -122,7 +122,7 @@ public class ResourceHandler extends Handler.Wrapper protected ResourceService.WelcomeFactory setupWelcomeFactory() { - return request -> + return (content, request) -> { if (_welcomes == null) return null; @@ -131,7 +131,7 @@ public class ResourceHandler extends Handler.Wrapper { String pathInContext = Request.getPathInContext(request); String welcomeInContext = URIUtil.addPaths(pathInContext, welcome); - Resource welcomePath = _baseResource.resolve(pathInContext).resolve(welcome); + Resource welcomePath = content.getResource().resolve(welcome); if (Resources.isReadableFile(welcomePath)) return welcomeInContext; } 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 4654ae5db3d..1a1995f17e4 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 @@ -1015,23 +1015,14 @@ public class DefaultServlet extends HttpServlet } @Override - public String getWelcomeTarget(Request coreRequest) + public String getWelcomeTarget(HttpContent content, Request coreRequest) { String[] welcomes = _servletContextHandler.getWelcomeFiles(); if (welcomes == null) return null; - - HttpServletRequest request = getServletRequest(coreRequest); String pathInContext = Request.getPathInContext(coreRequest); - String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); - String requestTarget; - if (includedServletPath != null) - requestTarget = getIncludedPathInContext(request, includedServletPath, isPathInfoOnly()); - else - requestTarget = isPathInfoOnly() ? request.getPathInfo() : pathInContext; - String welcomeTarget = null; - Resource base = _baseResource.resolve(requestTarget); + Resource base = content.getResource(); if (Resources.isReadableDirectory(base)) { for (String welcome : welcomes) @@ -1040,13 +1031,16 @@ public class DefaultServlet extends HttpServlet // If the welcome resource is a file, it has // precedence over resources served by Servlets. - Resource welcomePath = base.resolve(welcome); + Resource welcomePath = content.getResource().resolve(welcome); if (Resources.isReadableFile(welcomePath)) return welcomeInContext; // Check whether a Servlet may serve the welcome resource. if (_welcomeServletMode != WelcomeServletMode.NONE && welcomeTarget == null) { + if (isPathInfoOnly() && !isIncluded(getServletRequest(coreRequest))) + welcomeTarget = URIUtil.addPaths(getServletRequest(coreRequest).getPathInfo(), welcome); + ServletHandler.MappedServlet entry = _servletContextHandler.getServletHandler().getMappedServlet(welcomeInContext); // Is there a different Servlet that may serve the welcome resource? if (entry != null && entry.getServletHolder().getServletInstance() != DefaultServlet.this) diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java index c418bfe0455..262d50ae2c7 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java @@ -3185,6 +3185,63 @@ public class DefaultServletTest } } + @Test + public void testPathInfoOnly() throws Exception + { + ServletContextHandler context = new ServletContextHandler(null, "/c1", ServletContextHandler.NO_SESSIONS); + context.setWelcomeFiles(new String[]{"index.y", "index.x"}); + ServletHolder indexServlet = new ServletHolder("index-servlet", new HttpServlet() + { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().println(req.getRequestURI()); + resp.getWriter().println("testPathInfoOnly-OK"); + resp.getWriter().close(); + } + }); + + ServletMapping indexMapping = new ServletMapping(); + indexMapping.setServletName("index-servlet"); + indexMapping.setPathSpecs(new String[]{"*.x", "*.y"}); + context.getServletHandler().addServlet(indexServlet); + context.getServletHandler().addServletMapping(indexMapping); + + Path pathTest = MavenTestingUtils.getTestResourcePath("pathTest"); + + Path defaultDir = pathTest.resolve("default"); + ServletHolder slashHolder = new ServletHolder("default", new DefaultServlet()); + slashHolder.setInitParameter("redirectWelcome", "false"); + slashHolder.setInitParameter("welcomeServlets", "true"); + slashHolder.setInitParameter("pathInfoOnly", "false"); + slashHolder.setInitParameter("baseResource", defaultDir.toAbsolutePath().toString()); + context.addServlet(slashHolder, "/"); + + Path rDir = pathTest.resolve("rdir"); + ServletHolder rHolder = new ServletHolder("rdefault", new DefaultServlet()); + rHolder.setInitParameter("redirectWelcome", "false"); + rHolder.setInitParameter("welcomeServlets", "true"); + rHolder.setInitParameter("pathInfoOnly", "true"); + rHolder.setInitParameter("baseResource", rDir.toAbsolutePath().toString()); + context.addServlet(rHolder, "/r/*"); + + server.stop(); + server.setHandler(context); + server.start(); + String rawRequest = """ + GET /c1/r/ HTTP/1.1\r + Host: localhost\r + Connection: close\r + \r + """; + + String rawResponse = connector.getResponse(rawRequest); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertThat(response.getContent(), containsString("testPathInfoOnly-OK")); + } + public static class WriterFilter implements Filter { @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/resources/pathTest/default/dot b/jetty-ee10/jetty-ee10-servlet/src/test/resources/pathTest/default/dot new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/resources/pathTest/rdir/dot b/jetty-ee10/jetty-ee10-servlet/src/test/resources/pathTest/rdir/dot new file mode 100644 index 00000000000..e69de29bb2d From 44de5cd62bc606664cdf3f7841fa6740ae040090 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 22 Jun 2023 07:49:24 +1000 Subject: [PATCH 20/63] fix NPE from Response.checkSameSite() Signed-off-by: Lachlan Roberts --- .../src/main/java/org/eclipse/jetty/ee9/nested/Response.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java index 9ee7280294a..59429aa4a96 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java @@ -51,6 +51,7 @@ import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.content.HttpContent; import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.HttpCookieUtils.SetCookieHttpField; import org.eclipse.jetty.server.Session; @@ -253,7 +254,9 @@ public class Response implements HttpServletResponse return cookie; //sameSite is not set, use the default configured for the context, if one exists - SameSite contextDefault = HttpCookieUtils.getSameSiteDefault(_channel.getRequest().getContext().getCoreContext()); + ContextHandler.APIContext apiContext = _channel.getRequest().getContext(); + Context context = (apiContext == null) ? null : apiContext.getCoreContext(); + SameSite contextDefault = HttpCookieUtils.getSameSiteDefault(context); if (contextDefault == null) return cookie; //no default set From 9e99a58ac4cbf38cd897021bd3cade18c496bd3e Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 22 Jun 2023 09:08:29 +1000 Subject: [PATCH 21/63] Issue #9944 - remove integer from demand in websocket core Signed-off-by: Lachlan Roberts --- .../jetty/websocket/core/CoreSession.java | 9 ++++--- .../jetty/websocket/core/ExtensionStack.java | 16 ++++++------- .../jetty/websocket/core/FrameHandler.java | 8 +++---- .../websocket/core/WebSocketConnection.java | 9 +++---- .../websocket/core/WebSocketCoreSession.java | 4 ++-- .../core/internal/FragmentExtension.java | 7 +++--- .../core/internal/MessageHandler.java | 10 ++++---- .../internal/PerMessageDeflateExtension.java | 7 +++--- .../core/messages/AbstractMessageSink.java | 4 ++-- .../core/messages/ByteArrayMessageSink.java | 4 ++-- .../core/messages/ByteBufferMessageSink.java | 4 ++-- .../core/messages/MessageInputStream.java | 4 ++-- .../websocket/core/messages/MessageSink.java | 2 +- .../messages/PartialByteArrayMessageSink.java | 2 +- .../PartialByteBufferMessageSink.java | 2 +- .../core/messages/StringMessageSink.java | 2 +- .../websocket/core/util/DemandChain.java | 6 ++--- .../websocket/core/util/DemandingFlusher.java | 13 +++++----- .../jetty/websocket/core/DemandTest.java | 6 ++--- .../core/DemandingIncomingFramesCapture.java | 2 +- .../websocket/core/EchoFrameHandler.java | 2 +- .../websocket/core/TestAsyncFrameHandler.java | 4 ++-- .../websocket/core/TestFrameHandler.java | 2 +- .../websocket/core/WebSocketCloseTest.java | 24 +++++++++---------- .../websocket/core/WebSocketOpenTest.java | 6 ++--- .../client/WebSocketClientServerTest.java | 2 +- .../core/extensions/ExtensionTool.java | 6 ++--- .../extensions/FragmentExtensionTest.java | 6 ++--- .../PerMessageDeflateExtensionTest.java | 8 +++---- .../PermessageDeflateDemandTest.java | 4 ++-- .../websocket/core/proxy/WebSocketProxy.java | 8 +++---- .../core/server/WebSocketServerTest.java | 21 +++++++++------- .../common/JettyWebSocketFrameHandler.java | 2 +- .../websocket/common/WebSocketSession.java | 2 +- .../common/JakartaWebSocketFrameHandler.java | 12 +++++----- .../jakarta/common/AbstractSessionTest.java | 2 +- .../jakarta/tests/NetworkFuzzer.java | 4 ++-- .../tests/framehandlers/FrameEcho.java | 2 +- .../common/JakartaWebSocketFrameHandler.java | 12 +++++----- .../jakarta/common/AbstractSessionTest.java | 2 +- .../jakarta/tests/NetworkFuzzer.java | 4 ++-- .../tests/framehandlers/FrameEcho.java | 4 ++-- .../common/JettyWebSocketFrameHandler.java | 4 ++-- 43 files changed, 129 insertions(+), 135 deletions(-) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java index 51ebf73455e..29563420585 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java @@ -152,14 +152,13 @@ public interface CoreSession extends OutgoingFrames, IncomingFrames, Configurati void abort(); /** - *

Manages flow control by indicating demand for WebSocket frames.

+ *

Manages flow control by indicating demand for a WebSocket frame.

*

A call to {@link FrameHandler#onFrame(Frame, Callback)} will only * be made if there is demand.

* - * @param n the number of frames that can be handled in sequential calls to - * {@link FrameHandler#onFrame(Frame, Callback)}, must be positive. + * {@link FrameHandler#onFrame(Frame, Callback)}. */ - void demand(long n); + void demand(); /** * @return true if an extension has been negotiated which uses the RSV1 bit. @@ -287,7 +286,7 @@ public interface CoreSession extends OutgoingFrames, IncomingFrames, Configurati } @Override - public void demand(long n) + public void demand() { } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/ExtensionStack.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/ExtensionStack.java index c3a12bfdc08..58bdb6ad544 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/ExtensionStack.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/ExtensionStack.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ListIterator; -import java.util.function.LongConsumer; import java.util.stream.Collectors; import org.eclipse.jetty.http.BadMessageException; @@ -45,8 +44,8 @@ public class ExtensionStack implements IncomingFrames, OutgoingFrames, Dumpable private IncomingFrames incoming; private OutgoingFrames outgoing; private final Extension[] rsvClaims = new Extension[3]; - private LongConsumer lastDemand; - private DemandChain demandChain = n -> lastDemand.accept(n); + private DemandChain lastDemand; + private DemandChain demandChain = () -> lastDemand.demand(); public ExtensionStack(WebSocketComponents components, Behavior behavior) { @@ -224,10 +223,9 @@ public class ExtensionStack implements IncomingFrames, OutgoingFrames, Dumpable ext.setNextOutgoingFrames(outgoing); outgoing = ext; - if (ext instanceof DemandChain) + if (ext instanceof DemandChain demandingExtension) { - DemandChain demandingExtension = (DemandChain)ext; - demandingExtension.setNextDemand(demandChain::demand); + demandingExtension.setNextDemand(demandChain); demandChain = demandingExtension; } } @@ -273,12 +271,12 @@ public class ExtensionStack implements IncomingFrames, OutgoingFrames, Dumpable } } - public void demand(long n) + public void demand() { - demandChain.demand(n); + demandChain.demand(); } - public void setLastDemand(LongConsumer lastDemand) + public void setLastDemand(DemandChain lastDemand) { this.lastDemand = lastDemand; } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java index 3db3a5b5b3b..295c199d74c 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java @@ -54,7 +54,7 @@ import org.eclipse.jetty.util.Callback; * or not. The error code will indicate the nature of the close. * *

FrameHandler is responsible to manage the demand for more - * WebSocket frames, either directly by calling {@link CoreSession#demand(long)} + * WebSocket frames, either directly by calling {@link CoreSession#demand()} * or by delegating the demand management to other components.

*/ public interface FrameHandler extends IncomingFrames @@ -64,7 +64,7 @@ public interface FrameHandler extends IncomingFrames *

It is allowed to send WebSocket frames via * {@link CoreSession#sendFrame(Frame, Callback, boolean)}. *

WebSocket frames cannot be received until a call to - * {@link CoreSession#demand(long)} is made.

+ * {@link CoreSession#demand()} is made.

*

If the callback argument is failed, the implementation * sends a CLOSE frame with {@link CloseStatus#SERVER_ERROR}, * and the connection will be closed.

@@ -80,7 +80,7 @@ public interface FrameHandler extends IncomingFrames *

This method will never be called concurrently for the * same session; will be called sequentially to satisfy the * outstanding demand signaled by calls to - * {@link CoreSession#demand(long)}.

+ * {@link CoreSession#demand()}.

*

Both control and data frames are passed to this method.

*

CLOSE frames may be responded from this method, but if * they are not responded, then the implementation will respond @@ -89,7 +89,7 @@ public interface FrameHandler extends IncomingFrames * that the buffers associated with the frame can be recycled.

*

Additional WebSocket frames (of any type, including CLOSE * frames) cannot be received until a call to - * {@link CoreSession#demand(long)} is made.

+ * {@link CoreSession#demand()} is made.

* * @param frame the WebSocket frame. * @param callback the callback to indicate success or failure of diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java index 66d98529927..11909455d2e 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java @@ -343,21 +343,18 @@ public class WebSocketConnection extends AbstractConnection implements Connectio fillAndParse(); } - public void demand(long n) + public void demand() { - if (n <= 0) - throw new IllegalArgumentException("Demand must be positive"); - boolean fillAndParse = false; try (AutoLock l = lock.lock()) { if (LOG.isDebugEnabled()) - LOG.debug("demand {} d={} fp={} {} {}", n, demand, fillingAndParsing, networkBuffer, this); + LOG.debug("demand {} d={} fp={} {}", demand, fillingAndParsing, networkBuffer, this); if (demand < 0) return; - demand = MathUtils.cappedAdd(demand, n); + demand = MathUtils.cappedAdd(demand, 1); if (!fillingAndParsing) { diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketCoreSession.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketCoreSession.java index da5738982ae..32102dd7f6b 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketCoreSession.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketCoreSession.java @@ -402,9 +402,9 @@ public class WebSocketCoreSession implements CoreSession, Dumpable } @Override - public void demand(long n) + public void demand() { - getExtensionStack().demand(n); + getExtensionStack().demand(); } @Override diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/FragmentExtension.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/FragmentExtension.java index d41267f6dbe..b048421d35f 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/FragmentExtension.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/FragmentExtension.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.websocket.core.internal; import java.nio.ByteBuffer; -import java.util.function.LongConsumer; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.websocket.core.AbstractExtension; @@ -55,13 +54,13 @@ public class FragmentExtension extends AbstractExtension implements DemandChain } @Override - public void demand(long n) + public void demand() { - incomingFlusher.demand(n); + incomingFlusher.demand(); } @Override - public void setNextDemand(LongConsumer nextDemand) + public void setNextDemand(DemandChain nextDemand) { incomingFlusher.setNextDemand(nextDemand); } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/MessageHandler.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/MessageHandler.java index 81bf7f7585c..bdcad67dacc 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/MessageHandler.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/MessageHandler.java @@ -117,7 +117,7 @@ public class MessageHandler implements FrameHandler this.coreSession = coreSession; callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override @@ -210,7 +210,7 @@ public class MessageHandler implements FrameHandler callback.succeeded(); } - coreSession.demand(1); + coreSession.demand(); } catch (Throwable t) { @@ -244,7 +244,7 @@ public class MessageHandler implements FrameHandler callback.succeeded(); } - coreSession.demand(1); + coreSession.demand(); } catch (Throwable t) { @@ -264,13 +264,13 @@ public class MessageHandler implements FrameHandler protected void onPingFrame(Frame frame, Callback callback) { - coreSession.sendFrame(new Frame(OpCode.PONG, true, frame.getPayload()), Callback.from(() -> coreSession.demand(1), callback), false); + coreSession.sendFrame(new Frame(OpCode.PONG, true, frame.getPayload()), Callback.from(() -> coreSession.demand(), callback), false); } protected void onPongFrame(Frame frame, Callback callback) { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } protected void onCloseFrame(Frame frame, Callback callback) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/PerMessageDeflateExtension.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/PerMessageDeflateExtension.java index 14cb66a15e3..67cb1ebcb57 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/PerMessageDeflateExtension.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/PerMessageDeflateExtension.java @@ -17,7 +17,6 @@ import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.LongConsumer; import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.Inflater; @@ -252,15 +251,15 @@ public class PerMessageDeflateExtension extends AbstractExtension implements Dem } @Override - public void setNextDemand(LongConsumer nextDemand) + public void setNextDemand(DemandChain nextDemand) { incomingFlusher.setNextDemand(nextDemand); } @Override - public void demand(long n) + public void demand() { - incomingFlusher.demand(n); + incomingFlusher.demand(); } private class OutgoingFlusher extends TransformingFlusher diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java index 819b1a6154a..6a33b5a4c82 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java @@ -87,12 +87,12 @@ public abstract class AbstractMessageSink implements MessageSink /** *

If {@link #isAutoDemand()} then demands for one more WebSocket frame - * via {@link CoreSession#demand(long)}; otherwise it is a no-operation, + * via {@link CoreSession#demand()}; otherwise it is a no-operation, * because the demand is explicitly managed by the application function.

*/ protected void autoDemand() { if (isAutoDemand()) - getCoreSession().demand(1); + getCoreSession().demand(); } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteArrayMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteArrayMessageSink.java index f243a9cd49a..e6be28115aa 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteArrayMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteArrayMessageSink.java @@ -78,7 +78,7 @@ public class ByteArrayMessageSink extends AbstractMessageSink if (!frame.isFin() && !frame.hasPayload()) { callback.succeeded(); - getCoreSession().demand(1); + getCoreSession().demand(); return; } @@ -96,7 +96,7 @@ public class ByteArrayMessageSink extends AbstractMessageSink } else { - getCoreSession().demand(1); + getCoreSession().demand(); } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteBufferMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteBufferMessageSink.java index d6bc3fe34f6..afcec2dda7e 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteBufferMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ByteBufferMessageSink.java @@ -82,7 +82,7 @@ public class ByteBufferMessageSink extends AbstractMessageSink if (!frame.isFin() && !frame.hasPayload()) { callback.succeeded(); - getCoreSession().demand(1); + getCoreSession().demand(); return; } @@ -103,7 +103,7 @@ public class ByteBufferMessageSink extends AbstractMessageSink else { // Did not call the application so must explicitly demand here. - getCoreSession().demand(1); + getCoreSession().demand(); } } catch (Throwable t) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageInputStream.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageInputStream.java index afd93bde983..ecc7fefea71 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageInputStream.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageInputStream.java @@ -65,7 +65,7 @@ public class MessageInputStream extends InputStream implements MessageSink if (!frame.isFin() && !frame.hasPayload()) { callback.succeeded(); - session.demand(1); + session.demand(); return; } @@ -228,7 +228,7 @@ public class MessageInputStream extends InputStream implements MessageSink { current.callback.succeeded(); if (!current.frame.isFin()) - session.demand(1); + session.demand(); } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageSink.java index 896ae3baad2..7792327a19a 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageSink.java @@ -33,7 +33,7 @@ public interface MessageSink * payload is consumed.

*

The demand for more frames must be explicitly invoked, * or arranged to be invoked asynchronously, by the implementation - * of this method, by calling {@link CoreSession#demand(long)}.

+ * of this method, by calling {@link CoreSession#demand()}.

* * @param frame the frame to consume * @param callback the callback to complete when the frame is consumed diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteArrayMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteArrayMessageSink.java index fd34a4b7c68..c07b35484bc 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteArrayMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteArrayMessageSink.java @@ -54,7 +54,7 @@ public class PartialByteArrayMessageSink extends AbstractMessageSink else { callback.succeeded(); - getCoreSession().demand(1); + getCoreSession().demand(); } } catch (Throwable t) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteBufferMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteBufferMessageSink.java index cde238b5656..5cdf0ebba02 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteBufferMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/PartialByteBufferMessageSink.java @@ -52,7 +52,7 @@ public class PartialByteBufferMessageSink extends AbstractMessageSink else { callback.succeeded(); - getCoreSession().demand(1); + getCoreSession().demand(); } } catch (Throwable t) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/StringMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/StringMessageSink.java index 01650fc6447..ca7c6a063bb 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/StringMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/StringMessageSink.java @@ -72,7 +72,7 @@ public class StringMessageSink extends AbstractMessageSink else { callback.succeeded(); - getCoreSession().demand(1); + getCoreSession().demand(); } } catch (Throwable t) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandChain.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandChain.java index c0cc0ecee47..bf85b5b83d2 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandChain.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandChain.java @@ -13,8 +13,6 @@ package org.eclipse.jetty.websocket.core.util; -import java.util.function.LongConsumer; - import org.eclipse.jetty.websocket.core.Extension; import org.eclipse.jetty.websocket.core.ExtensionStack; @@ -25,9 +23,9 @@ import org.eclipse.jetty.websocket.core.ExtensionStack; */ public interface DemandChain { - void demand(long n); + void demand(); - default void setNextDemand(LongConsumer nextDemand) + default void setNextDemand(DemandChain nextDemand) { } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java index 9c86efc33da..be55fc88d42 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java @@ -15,7 +15,6 @@ package org.eclipse.jetty.websocket.core.util; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.LongConsumer; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.CountingCallback; @@ -44,7 +43,7 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema private final IncomingFrames _emitFrame; private final AtomicLong _demand = new AtomicLong(); private final AtomicReference _failure = new AtomicReference<>(); - private LongConsumer _nextDemand; + private DemandChain _nextDemand; private Frame _frame; private Callback _callback; @@ -76,21 +75,21 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema protected abstract boolean handle(Frame frame, Callback callback, boolean first); @Override - public void demand(long n) + public void demand() { - _demand.getAndUpdate(d -> Math.addExact(d, n)); + _demand.incrementAndGet(); iterate(); } @Override - public void setNextDemand(LongConsumer nextDemand) + public void setNextDemand(DemandChain nextDemand) { _nextDemand = nextDemand; } /** * Used to supply the flusher with a new frame. This frame should only arrive if demanded - * through the {@link LongConsumer} provided by {@link #setNextDemand(LongConsumer)}. + * through the {@link DemandChain} provided by {@link #setNextDemand(DemandChain)}. * @param frame the WebSocket frame. * @param callback to release frame payload. */ @@ -160,7 +159,7 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema if (_needContent) { _needContent = false; - _nextDemand.accept(1); + _nextDemand.demand(); return Action.SCHEDULED; } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandTest.java index bdf95686984..77c0a546370 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandTest.java @@ -69,14 +69,14 @@ public class DemandTest { _coreSession = coreSession; callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override public void onFrame(Frame frame, Callback callback) { callback.succeeded(); - _coreSession.demand(1); + _coreSession.demand(); } @Override @@ -111,7 +111,7 @@ public class DemandTest _coreSession.abort(); // Demand should not throw even if closed. - _coreSession.demand(1); + _coreSession.demand(); errorFuture.complete(null); } catch (Throwable t) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandingIncomingFramesCapture.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandingIncomingFramesCapture.java index 365021d011f..b66c219123b 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandingIncomingFramesCapture.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/DemandingIncomingFramesCapture.java @@ -33,7 +33,7 @@ public class DemandingIncomingFramesCapture extends IncomingFramesCapture } finally { - _coreSession.demand(1); + _coreSession.demand(); } } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/EchoFrameHandler.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/EchoFrameHandler.java index 0f3ff967f13..7f21f05ac41 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/EchoFrameHandler.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/EchoFrameHandler.java @@ -56,6 +56,6 @@ public class EchoFrameHandler extends TestAsyncFrameHandler callback.succeeded(); } - coreSession.demand(1); + coreSession.demand(); } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java index 5715268773d..b6cc1cce0a6 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestAsyncFrameHandler.java @@ -51,7 +51,7 @@ public class TestAsyncFrameHandler implements FrameHandler LOG.debug("[{}] onOpen {}", name, coreSession); this.coreSession = coreSession; callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); openLatch.countDown(); } @@ -62,7 +62,7 @@ public class TestAsyncFrameHandler implements FrameHandler LOG.debug("[{}] onFrame {}", name, frame); receivedFrames.offer(Frame.copy(frame)); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java index 7fb8b865b06..d4f1ef133da 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java @@ -71,7 +71,7 @@ public class TestFrameHandler implements SynchronousFrameHandler protected void demand() { - coreSession.demand(1); + coreSession.demand(); } @Override diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java index 8d4d7c95426..66608a7dcd7 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java @@ -111,7 +111,7 @@ public class WebSocketCloseTest extends WebSocketTester case ISHUT: { client.getOutputStream().write(RawFrameBuilder.buildClose(new CloseStatus(CloseStatus.NORMAL), true)); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); Frame frame = serverHandler.receivedFrames.poll(5, TimeUnit.SECONDS); assertThat(new CloseStatus(frame.getPayload()).getCode(), is(CloseStatus.NORMAL)); assertThat(serverHandler.coreSession.toString(), containsString("ISHUT")); @@ -193,7 +193,7 @@ public class WebSocketCloseTest extends WebSocketTester public void testClientCloseOSHUT(String scheme) throws Exception { setup(State.OSHUT, scheme); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); client.getOutputStream().write(RawFrameBuilder.buildClose(new CloseStatus(CloseStatus.NORMAL), true)); assertNotNull(serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS)); serverHandler.receivedCallback.poll().succeeded(); @@ -209,7 +209,7 @@ public class WebSocketCloseTest extends WebSocketTester public void testClientDifferentCloseOSHUT(String scheme) throws Exception { setup(State.OSHUT, scheme); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); client.getOutputStream().write(RawFrameBuilder.buildClose(new CloseStatus(CloseStatus.BAD_PAYLOAD), true)); assertNotNull(serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS)); serverHandler.receivedCallback.poll().succeeded(); @@ -227,7 +227,7 @@ public class WebSocketCloseTest extends WebSocketTester try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class)) { setup(State.OSHUT, scheme); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); client.getOutputStream().write(RawFrameBuilder.buildClose(new CloseStatus(CloseStatus.NORMAL), true)); assertNotNull(serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS)); serverHandler.receivedCallback.poll().failed(new Exception("Test")); @@ -246,7 +246,7 @@ public class WebSocketCloseTest extends WebSocketTester setup(State.OPEN, scheme); client.getOutputStream().write(RawFrameBuilder.buildFrame(OpCode.PONG, "pong frame not masked", false)); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); assertThat(serverHandler.closeStatus.getCode(), is(CloseStatus.PROTOCOL)); assertThat(serverHandler.closeStatus.getReason(), containsString("Client MUST mask all frames")); @@ -259,7 +259,7 @@ public class WebSocketCloseTest extends WebSocketTester setup(State.OSHUT, scheme); client.getOutputStream().write(RawFrameBuilder.buildFrame(OpCode.PONG, "pong frame not masked", false)); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); assertThat(serverHandler.closeStatus.getCode(), is(CloseStatus.PROTOCOL)); assertThat(serverHandler.closeStatus.getReason(), containsString("Client MUST mask all frames")); @@ -335,7 +335,7 @@ public class WebSocketCloseTest extends WebSocketTester client.close(); assertFalse(serverHandler.closed.await(250, TimeUnit.MILLISECONDS)); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); assertThat(serverHandler.closeStatus.getCode(), is(CloseStatus.NO_CLOSE)); } @@ -348,7 +348,7 @@ public class WebSocketCloseTest extends WebSocketTester client.close(); assertFalse(serverHandler.closed.await(250, TimeUnit.MILLISECONDS)); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); assertThat(serverHandler.closeStatus.getCode(), is(CloseStatus.NO_CLOSE)); } @@ -376,7 +376,7 @@ public class WebSocketCloseTest extends WebSocketTester try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketCoreSession.class)) { - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); } @@ -394,7 +394,7 @@ public class WebSocketCloseTest extends WebSocketTester try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketCoreSession.class)) { - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); } @@ -446,7 +446,7 @@ public class WebSocketCloseTest extends WebSocketTester client.getOutputStream().write(RawFrameBuilder.buildClose( new CloseStatus(CloseStatus.NORMAL, "normal response 1"), true)); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertNotNull(serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS)); Callback closeFrameCallback = Objects.requireNonNull(serverHandler.receivedCallback.poll()); closeFrameCallback.succeeded(); @@ -517,7 +517,7 @@ public class WebSocketCloseTest extends WebSocketTester CloseStatus closeStatus = new CloseStatus(CloseStatus.NORMAL, "throw from onFrame"); client.getOutputStream().write(RawFrameBuilder.buildClose(closeStatus, true)); - serverHandler.coreSession.demand(1); + serverHandler.coreSession.demand(); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); assertThat(serverHandler.closeStatus.getCode(), is(CloseStatus.SERVER_ERROR)); assertThat(serverHandler.closeStatus.getReason(), containsString("deliberately throwing from onFrame")); diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java index 8012efe24db..d20196c589d 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java @@ -65,7 +65,7 @@ public class WebSocketOpenTest extends WebSocketTester assertThat(s.toString(), containsString("CONNECTED")); s.sendFrame(new Frame(OpCode.TEXT, "Hello"), NOOP, false); c.succeeded(); - s.demand(1); + s.demand(); }); Frame.Parsed frame = receiveFrame(client.getInputStream()); assertThat(frame.getPayloadAsUTF8(), is("Hello")); @@ -155,7 +155,7 @@ public class WebSocketOpenTest extends WebSocketTester // Demanding in onOpen will allow you to receive frames. client.getOutputStream().write(RawFrameBuilder.buildFrame(OpCode.TEXT, "message in onOpen", true)); assertNull(serverHandler.receivedFrames.poll(1, TimeUnit.SECONDS)); - coreSession.demand(1); + coreSession.demand(); Frame rcvFrame = serverHandler.receivedFrames.poll(5, TimeUnit.SECONDS); assertNotNull(rcvFrame); assertThat(rcvFrame.getPayloadAsUTF8(), is("message in onOpen")); @@ -163,7 +163,7 @@ public class WebSocketOpenTest extends WebSocketTester // Demand to receive the close frame. client.getOutputStream().write(RawFrameBuilder.buildClose(new CloseStatus(CloseStatus.NORMAL), true)); assertFalse(serverHandler.closeLatch.await(1, TimeUnit.SECONDS)); - coreSession.demand(1); + coreSession.demand(); assertTrue(serverHandler.closeLatch.await(5, TimeUnit.SECONDS)); // Closed handled normally diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java index 0638c6e000a..a164569fbb8 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java @@ -104,7 +104,7 @@ public class WebSocketClientServerTest else { callback.succeeded(); - getCoreSession().demand(1); + getCoreSession().demand(); } } }; diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java index e3359d513f8..d350c12e28e 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java @@ -82,7 +82,7 @@ public class ExtensionTool byte[] net; // Simulate initial demand from onOpen(). - coreSession.demand(1); + coreSession.demand(); for (int i = 0; i < parts; i++) { @@ -102,7 +102,7 @@ public class ExtensionTool public void succeeded() { super.succeeded(); - coreSession.demand(1); + coreSession.demand(); } }; ext.onFrame(frame, callback); @@ -172,7 +172,7 @@ public class ExtensionTool private WebSocketCoreSession newWebSocketCoreSession(List configs) { ExtensionStack exStack = new ExtensionStack(components, Behavior.SERVER); - exStack.setLastDemand(l -> {}); // Never delegate to WebSocketConnection as it is null for this test. + exStack.setLastDemand(() -> {}); // Never delegate to WebSocketConnection as it is null for this test. exStack.negotiate(configs, configs); return new WebSocketCoreSession(new TestMessageHandler(), Behavior.SERVER, Negotiated.from(exStack), components); } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/FragmentExtensionTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/FragmentExtensionTest.java index 15110d91c04..c19b7aa0dcc 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/FragmentExtensionTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/FragmentExtensionTest.java @@ -59,7 +59,7 @@ public class FragmentExtensionTest extends AbstractExtensionTest ext.setNextIncomingFrames(capture); // Simulate initial demand from onOpen(). - coreSession.demand(1); + coreSession.demand(); // Quote List quote = new ArrayList<>(); @@ -131,7 +131,7 @@ public class FragmentExtensionTest extends AbstractExtensionTest ext.setNextIncomingFrames(capture); // Simulate initial demand from onOpen(). - coreSession.demand(1); + coreSession.demand(); String payload = "Are you there?"; Frame ping = new Frame(OpCode.PING).setPayload(payload); @@ -333,7 +333,7 @@ public class FragmentExtensionTest extends AbstractExtensionTest { ExtensionStack exStack = new ExtensionStack(components, Behavior.SERVER); exStack.negotiate(configs, configs); - exStack.setLastDemand(l -> {}); // Never delegate to WebSocketConnection as it is null for this test. + exStack.setLastDemand(() -> {}); // Never delegate to WebSocketConnection as it is null for this test. WebSocketCoreSession coreSession = new WebSocketCoreSession(new TestMessageHandler(), Behavior.SERVER, Negotiated.from(exStack), components); configuration.customize(configuration); return coreSession; diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java index 6db995869b9..1ab713aac6b 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflateExtensionTest.java @@ -295,7 +295,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest ext.setNextIncomingFrames(capture); // Simulate initial demand from onOpen(). - coreSession.demand(1); + coreSession.demand(); String payload = "Are you there?"; Frame ping = new Frame(OpCode.PING).setPayload(payload); @@ -333,7 +333,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest ext.setNextIncomingFrames(capture); // Simulate initial demand from onOpen(). - coreSession.demand(1); + coreSession.demand(); // Quote List quote = new ArrayList<>(); @@ -386,7 +386,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest ext.setNextIncomingFrames(capture); // Simulate initial demand from onOpen(). - coreSession.demand(1); + coreSession.demand(); Frame ping = new Frame(OpCode.TEXT); ping.setRsv1(true); @@ -613,7 +613,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest { ExtensionStack exStack = new ExtensionStack(components, Behavior.SERVER); exStack.negotiate(configs, configs); - exStack.setLastDemand(l -> {}); // Never delegate to WebSocketConnection as it is null for this test. + exStack.setLastDemand(() -> {}); // Never delegate to WebSocketConnection as it is null for this test. WebSocketCoreSession coreSession = new WebSocketCoreSession(new TestMessageHandler(), Behavior.SERVER, Negotiated.from(exStack), components); configuration.customize(configuration); return coreSession; diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PermessageDeflateDemandTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PermessageDeflateDemandTest.java index 5f3284ff0eb..ca91ccfbd52 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PermessageDeflateDemandTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PermessageDeflateDemandTest.java @@ -120,7 +120,7 @@ public class PermessageDeflateDemandTest { _coreSession = coreSession; callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override @@ -170,7 +170,7 @@ public class PermessageDeflateDemandTest callback.succeeded(); } - _coreSession.demand(1); + _coreSession.demand(); } @Override diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java index a5484a9c1d1..085c80732b7 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/proxy/WebSocketProxy.java @@ -137,7 +137,7 @@ class WebSocketProxy else { callback.succeeded(); - client2ProxySession.demand(1); + client2ProxySession.demand(); } } @@ -226,7 +226,7 @@ class WebSocketProxy proxy2Server.send(frame, Callback.from(() -> { c.succeeded(); - client2ProxySession.demand(1); + client2ProxySession.demand(); }, c::failed)); } else @@ -471,7 +471,7 @@ class WebSocketProxy else { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } } @@ -591,7 +591,7 @@ class WebSocketProxy client2Proxy.send(frame, Callback.from(() -> { c.succeeded(); - proxy2ServerSession.demand(1); + proxy2ServerSession.demand(); }, c::failed)); } else diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java index d8e9483ade8..57cc99a7ea6 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java @@ -110,7 +110,8 @@ public class WebSocketServerTest extends WebSocketTester Frame frame = serverHandler.receivedFrames.poll(250, TimeUnit.MILLISECONDS); assertNull(frame); - serverHandler.getCoreSession().demand(2); + serverHandler.getCoreSession().demand(); + serverHandler.getCoreSession().demand(); frame = serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS); assertNotNull(frame); @@ -124,7 +125,7 @@ public class WebSocketServerTest extends WebSocketTester client.getOutputStream().write(RawFrameBuilder.buildClose(CloseStatus.NORMAL_STATUS, true)); assertFalse(serverHandler.closed.await(250, TimeUnit.MILLISECONDS)); - serverHandler.getCoreSession().demand(1); + serverHandler.getCoreSession().demand(); assertTrue(serverHandler.closed.await(10, TimeUnit.SECONDS)); frame = serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS); assertNotNull(frame); @@ -148,7 +149,7 @@ public class WebSocketServerTest extends WebSocketTester public void onOpen(CoreSession coreSession, Callback callback) { super.onOpen(coreSession, callback); - coreSession.demand(1); + coreSession.demand(); } @Override @@ -157,7 +158,7 @@ public class WebSocketServerTest extends WebSocketTester LOG.info("onFrame: " + BufferUtil.toDetailString(frame.getPayload())); receivedFrames.offer(frame); receivedCallbacks.offer(callback); - getCoreSession().demand(1); + getCoreSession().demand(); } }; @@ -248,7 +249,9 @@ public class WebSocketServerTest extends WebSocketTester public void onOpen(CoreSession coreSession, Callback callback) { super.onOpen(coreSession, callback); - coreSession.demand(3); + coreSession.demand(); + coreSession.demand(); + coreSession.demand(); } @Override @@ -289,7 +292,7 @@ public class WebSocketServerTest extends WebSocketTester client.close(); assertFalse(serverHandler.closed.await(250, TimeUnit.MILLISECONDS)); - serverHandler.getCoreSession().demand(1); + serverHandler.getCoreSession().demand(); assertTrue(serverHandler.closed.await(10, TimeUnit.SECONDS)); } } @@ -307,7 +310,8 @@ public class WebSocketServerTest extends WebSocketTester { super.onOpen(coreSession); callback.succeeded(); - coreSession.demand(2); + coreSession.demand(); + coreSession.demand(); } @Override @@ -375,7 +379,8 @@ public class WebSocketServerTest extends WebSocketTester { super.onOpen(coreSession); callback.succeeded(); - coreSession.demand(2); + coreSession.demand(); + coreSession.demand(); } @Override diff --git a/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java b/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java index 9d9665dd49a..5a998f9806b 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java +++ b/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java @@ -402,7 +402,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler private void autoDemand() { if (isAutoDemand()) - session.getCoreSession().demand(1); + session.getCoreSession().demand(); } public String toString() diff --git a/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java b/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java index 8999602a90d..79ad7521625 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java +++ b/jetty-core/jetty-websocket/jetty-websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java @@ -55,7 +55,7 @@ public class WebSocketSession implements Session, Dumpable { if (frameHandler.isAutoDemand()) throw new IllegalStateException("auto-demanding endpoint cannot explicitly demand"); - coreSession.demand(1); + coreSession.demand(); } @Override diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketFrameHandler.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketFrameHandler.java index 8527b3a6951..30681596212 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketFrameHandler.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketFrameHandler.java @@ -178,7 +178,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler container.notifySessionListeners((listener) -> listener.onJakartaWebSocketSessionOpened(session)); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } catch (Throwable cause) { @@ -578,7 +578,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler if (activeMessageSink == null) { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); return; } @@ -593,12 +593,12 @@ public class JakartaWebSocketFrameHandler implements FrameHandler coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(frame.getPayload()), Callback.from(() -> { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); }, x -> { // Ignore failures, as we might be OSHUT but receive a PING. callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); }), false); } @@ -616,7 +616,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler JakartaWebSocketPongMessage pongMessage = new JakartaWebSocketPongMessage(payload); pongHandle.invoke(pongMessage); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } catch (Throwable cause) { @@ -626,7 +626,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler else { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } } diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/common/AbstractSessionTest.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/common/AbstractSessionTest.java index d759362e836..4f0b2e7359f 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/common/AbstractSessionTest.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/common/AbstractSessionTest.java @@ -81,7 +81,7 @@ public abstract class AbstractSessionTest } @Override - public void demand(long n) + public void demand() { demand.release(); } diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/NetworkFuzzer.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/NetworkFuzzer.java index bb127c34cbc..6acedda0209 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/NetworkFuzzer.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/NetworkFuzzer.java @@ -229,7 +229,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab this.coreSession = coreSession; this.openLatch.countDown(); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override @@ -237,7 +237,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab { receivedFrames.offer(Frame.copy(frame)); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/framehandlers/FrameEcho.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/framehandlers/FrameEcho.java index 0a8de6276e2..4618a7bb994 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/framehandlers/FrameEcho.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/framehandlers/FrameEcho.java @@ -32,7 +32,7 @@ public class FrameEcho implements FrameHandler { this.coreSession = coreSession; callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketFrameHandler.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketFrameHandler.java index feaa31fe733..a7d36d2412a 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketFrameHandler.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketFrameHandler.java @@ -179,7 +179,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler container.notifySessionListeners((listener) -> listener.onJakartaWebSocketSessionOpened(session)); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } catch (Throwable cause) { @@ -585,7 +585,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler if (activeMessageSink == null) { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); return; } @@ -600,12 +600,12 @@ public class JakartaWebSocketFrameHandler implements FrameHandler coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(frame.getPayload()), Callback.from(() -> { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); }, x -> { // Ignore failures, as we might be OSHUT but receive a PING. callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); }), false); } @@ -623,7 +623,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler JakartaWebSocketPongMessage pongMessage = new JakartaWebSocketPongMessage(payload); pongHandle.invoke(pongMessage); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } catch (Throwable cause) { @@ -633,7 +633,7 @@ public class JakartaWebSocketFrameHandler implements FrameHandler else { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } } diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/common/AbstractSessionTest.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/common/AbstractSessionTest.java index aa559af2a14..32d8c4760cb 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/common/AbstractSessionTest.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/common/AbstractSessionTest.java @@ -75,7 +75,7 @@ public abstract class AbstractSessionTest } @Override - public void demand(long n) + public void demand() { demand.release(); } diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/NetworkFuzzer.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/NetworkFuzzer.java index 201b31b9dda..f5e75693dbd 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/NetworkFuzzer.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/NetworkFuzzer.java @@ -229,7 +229,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab this.coreSession = coreSession; this.openLatch.countDown(); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override @@ -237,7 +237,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab { receivedFrames.offer(Frame.copy(frame)); callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/framehandlers/FrameEcho.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/framehandlers/FrameEcho.java index 79314e3c1cf..a3ff7d5e45d 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/framehandlers/FrameEcho.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/framehandlers/FrameEcho.java @@ -32,7 +32,7 @@ public class FrameEcho implements FrameHandler { this.coreSession = coreSession; callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); } @Override @@ -41,7 +41,7 @@ public class FrameEcho implements FrameHandler Runnable succeedAndDemand = () -> { callback.succeeded(); - coreSession.demand(1); + coreSession.demand(); }; if (frame.isControlFrame()) diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-common/src/main/java/org/eclipse/jetty/ee9/websocket/common/JettyWebSocketFrameHandler.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-common/src/main/java/org/eclipse/jetty/ee9/websocket/common/JettyWebSocketFrameHandler.java index 9a348fd0811..6acd91608a0 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-common/src/main/java/org/eclipse/jetty/ee9/websocket/common/JettyWebSocketFrameHandler.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-common/src/main/java/org/eclipse/jetty/ee9/websocket/common/JettyWebSocketFrameHandler.java @@ -473,7 +473,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler if (frame != null) onFrame(frame, callback); else - session.getCoreSession().demand(1); + session.getCoreSession().demand(); } } @@ -498,7 +498,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler } if (demand) - session.getCoreSession().demand(1); + session.getCoreSession().demand(); } public static Throwable convertCause(Throwable cause) From 3f159fcf32860344b328bb00cf6869108a92df72 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Thu, 22 Jun 2023 10:25:12 +0200 Subject: [PATCH 22/63] Issue #9925 Also use current context for alternate getServletContextHandler method (#9941) --- .../org/eclipse/jetty/ee10/servlet/ServletContextHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java index cf2fba2d306..7bbc574a25f 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java @@ -157,6 +157,9 @@ public class ServletContextHandler extends ContextHandler { if (servletContext instanceof ServletContextApi servletContextApi) return servletContextApi.getContext().getServletContextHandler(); + ServletContextHandler sch = getCurrentServletContextHandler(); + if (sch != null) + return sch; throw new IllegalStateException("No Jetty ServletContextHandler, " + purpose + " unavailable"); } From 73dfac97325f6e65621de0f68a6aaa1186a98d6b Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Thu, 22 Jun 2023 02:47:53 -0700 Subject: [PATCH 23/63] Use non-deprecated WOFF MIME type (#9922) Signed-off-by: Basil Crow --- .../src/main/resources/org/eclipse/jetty/http/mime.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties b/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties index f689e3c239e..5d2cb60aa10 100644 --- a/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties +++ b/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties @@ -178,7 +178,7 @@ wml=text/vnd.wap.wml wmlc=application/vnd.wap.wmlc wmls=text/vnd.wap.wmlscript wmlsc=application/vnd.wap.wmlscriptc -woff=application/font-woff +woff=font/woff woff2=font/woff2 wrl=model/vrml wtls-ca-certificate=application/vnd.wap.wtls-ca-certificate From 0ffeb87817026514929bdc3c8ce2b8eff962c583 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 22 Jun 2023 20:56:14 +1000 Subject: [PATCH 24/63] use fields in nested Request instead of attributes for last context Signed-off-by: Lachlan Roberts --- .../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 b45ea51b149..c270128d6cb 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.eclipse.jetty.util.security.SecurityUtils; 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 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie (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 141fc78fce5..eb86c025fd8 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.eclipse.jetty.util.StringUtil; 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 class ErrorHandler extends AbstractHandler // 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 38ed7c9144f..843745510c4 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 java.util.stream.Stream; 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 @@ import static org.eclipse.jetty.util.thread.Invocable.InvocationType.NON_BLOCKIN 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 class HttpChannel implements Runnable, HttpOutput.Interceptor 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 75c57d36dca..ce17bdea656 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_MESSAGE; 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 class HttpChannelState 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 294f9a655de..8bf16a94abe 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 class Request implements HttpServletRequest 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 class Request implements HttpServletRequest { _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 6ee23353b9f..c192fce8108 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.eclipse.jetty.util.thread.AutoLock; 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 class ServletHandler extends ScopedHandler 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 8bf16a94abe..b978e217849 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 class Request implements HttpServletRequest { 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 | 116 ++++--- .../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 | 302 ++++++++---------- .../jetty/ee10/servlet/ServletChannel.java | 186 +++++++---- .../ee10/servlet/ServletContextHandler.java | 121 ++++++- .../ee10/servlet/ServletContextRequest.java | 101 +++--- .../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, 1314 insertions(+), 941 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 393732e4c21..26a2b4969c5 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 @@ public interface HttpFields extends Iterable, Supplier // 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 interface HttpFields extends Iterable, Supplier 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 8e1b6f1fbb7..6d848fca80c 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 class HttpFieldsTest return Stream.of( HttpFields.build(), HttpFields.build(0), + new HttpFields.Mutable.Wrapper(HttpFields.build()), new HttpFields.Mutable() { private final HttpFields.Mutable fields = HttpFields.build(); 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 bdd623f7829..48d998ddf6a 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 @@ public class ResourceService 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 @@ public class ResourceService 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 e74dd489361..f9e10e33dfe 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 class GzipHandler extends Handler.Wrapper implements GzipFactory if (Request.as(request, GzipRequest.class) != null) return next.handle(request, response, callback); - String path = Request.getPathInContext(request); - boolean tryInflate = getInflateBufferSize() >= 0 && isPathInflatable(path); - boolean tryDeflate = _methods.test(request.getMethod()) && isPathDeflatable(path) && isMimeTypeDeflatable(request.getContext().getMimeTypes(), path); + String pathInContext = Request.getPathInContext(request); + boolean tryInflate = getInflateBufferSize() >= 0 && isPathInflatable(pathInContext); + boolean tryDeflate = _methods.test(request.getMethod()) && isPathDeflatable(pathInContext) && isMimeTypeDeflatable(request.getContext().getMimeTypes(), pathInContext); // Can we skip looking at the request and wrapping request or response? if (!tryInflate && !tryDeflate) @@ -624,15 +624,15 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory /** * Test if the provided Request URI is allowed to be inflated based on the Path Specs filters. * - * @param requestURI the request uri + * @param pathInContext the request path in context * @return whether decompressing is allowed for the given the path. */ - protected boolean isPathInflatable(String requestURI) + protected boolean isPathInflatable(String pathInContext) { - if (requestURI == null) + if (pathInContext == null) return true; - return _inflatePaths.test(requestURI); + return _inflatePaths.test(pathInContext); } /** 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 31173e8cf4b..f4315df47da 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 @@ public class HttpChannelState implements HttpChannel, Components private final HandlerInvoker _handlerInvoker = new HandlerInvoker(); private final ConnectionMetaData _connectionMetaData; private final SerializedInvoker _serializedInvoker; - private final Attributes _requestAttributes = new Attributes.Lazy(); private final ResponseHttpFields _responseHeaders = new ResponseHttpFields(); private Thread _handling; private boolean _handled; @@ -157,7 +156,6 @@ public class HttpChannelState implements HttpChannel, Components _streamSendState = StreamSendState.SENDING; // Recycle. - _requestAttributes.clearAttributes(); _responseHeaders.reset(); _handling = null; _handled = false; @@ -741,6 +739,7 @@ public class HttpChannelState implements HttpChannel, Components private final MetaData.Request _metaData; private final AutoLock _lock; private final LongAdder _contentBytesRead = new LongAdder(); + private final Attributes _attributes = new Attributes.Lazy(); private HttpChannelState _httpChannelState; private Request _loggedRequest; private HttpFields _trailers; @@ -777,26 +776,25 @@ public class HttpChannelState implements HttpChannel, Components @Override public Object getAttribute(String name) { - HttpChannelState httpChannel = getHttpChannelState(); if (name.startsWith("org.eclipse.jetty")) { if (Server.class.getName().equals(name)) - return httpChannel.getConnectionMetaData().getConnector().getServer(); + return getConnectionMetaData().getConnector().getServer(); if (HttpChannelState.class.getName().equals(name)) - return httpChannel; + return getHttpChannelState(); // TODO: is the instanceof needed? // TODO: possibly remove this if statement or move to Servlet. if (HttpConnection.class.getName().equals(name) && getConnectionMetaData().getConnection() instanceof HttpConnection) return getConnectionMetaData().getConnection(); } - return httpChannel._requestAttributes.getAttribute(name); + return _attributes.getAttribute(name); } @Override public Object removeAttribute(String name) { - return getHttpChannelState()._requestAttributes.removeAttribute(name); + return _attributes.removeAttribute(name); } @Override @@ -804,19 +802,19 @@ public class HttpChannelState implements HttpChannel, Components { if (Server.class.getName().equals(name) || HttpChannelState.class.getName().equals(name) || HttpConnection.class.getName().equals(name)) return null; - return getHttpChannelState()._requestAttributes.setAttribute(name, attribute); + return _attributes.setAttribute(name, attribute); } @Override public Set getAttributeNameSet() { - return getHttpChannelState()._requestAttributes.getAttributeNameSet(); + return _attributes.getAttributeNameSet(); } @Override public void clearAttributes() { - getHttpChannelState()._requestAttributes.clearAttributes(); + _attributes.clearAttributes(); } @Override @@ -837,7 +835,7 @@ public class HttpChannelState implements HttpChannel, Components return _connectionMetaData; } - HttpChannelState getHttpChannelState() + private HttpChannelState getHttpChannelState() { try (AutoLock ignore = _lock.lock()) { 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 529f612c3d3..2aa2c1814b7 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 class Blocker /** * 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 1341f0147c5..5d4b2c58ff3 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 24ed32ad407..00000000000 --- 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 3a024c95902..41c7106dc5a 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 class ServerUpgradeResponseImpl extends Response.Wrapper implements Serve @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 07dbae33312..22b5b377f6e 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; + } - if (HttpHeader.SEC_WEBSOCKET_EXTENSIONS.is(name)) - { - _response.addExtensions(ExtensionConfig.parseList(value)); - return false; - } + case SEC_WEBSOCKET_EXTENSIONS -> + { + _response.removeExtensions(ExtensionConfig.parseList(field.getValue())); + yield true; + } - return super.onAddField(name, value); + default -> super.onRemoveField(field); + }; + } + return super.onRemoveField(field); } @Override - public boolean onRemoveField(String name) + public Mutable put(HttpField field) { - 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 (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)) - { - // TODO: why add extensions?? - _response.addExtensions(Collections.emptyList()); - return false; - } + _response.setExtensions(Collections.emptyList()); + return super.put(name, value); + } - return super.onRemoveField(name); + @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 Mutable put(HttpHeader header, String 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 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)) + _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 f173cba11c1..b222c8ddd71 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 @@ class AsyncContentProducer implements ContentProducer 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 @@ class AsyncContentProducer implements ContentProducer private boolean consumeAvailableChunks() { - return _servletChannel.getServletContextRequest().consumeAvailable(); + return _servletChannel.getRequest().consumeAvailable(); } @Override @@ -189,7 +189,7 @@ class AsyncContentProducer implements ContentProducer assertLocked(); if (LOG.isDebugEnabled()) LOG.debug("onContentProducible {}", this); - return _servletChannel.getState().onReadReady(); + return _servletChannel.getServletRequestState().onReadReady(); } @Override @@ -200,7 +200,7 @@ class AsyncContentProducer implements ContentProducer if (LOG.isDebugEnabled()) LOG.debug("nextChunk = {} {}", chunk, this); if (chunk != null) - _servletChannel.getState().onReadIdle(); + _servletChannel.getServletRequestState().onReadIdle(); return chunk; } @@ -227,8 +227,8 @@ class AsyncContentProducer implements ContentProducer return true; } - _servletChannel.getState().onReadUnready(); - _servletChannel.getServletContextRequest().demand(() -> + _servletChannel.getServletRequestState().onReadUnready(); + _servletChannel.getRequest().demand(() -> { if (_servletChannel.getHttpInput().onContentProducible()) _servletChannel.handle(); @@ -241,7 +241,7 @@ class AsyncContentProducer implements ContentProducer boolean isUnready() { - return _servletChannel.getState().isInputUnready(); + return _servletChannel.getServletRequestState().isInputUnready(); } private Content.Chunk produceChunk() @@ -280,7 +280,7 @@ class AsyncContentProducer implements ContentProducer } else { - _servletChannel.getState().onContentAdded(); + _servletChannel.getServletRequestState().onContentAdded(); } } @@ -297,7 +297,7 @@ class AsyncContentProducer implements ContentProducer 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 @@ class AsyncContentProducer implements ContentProducer _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 0271a05675f..29c65d004ec 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 class AsyncContextState implements AsyncContext 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 20d93f2e031..c999ad13e9d 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 @@ public class DebugListener extends AbstractLifeCycle implements ServletContextLi 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 class DebugListener extends AbstractLifeCycle implements ServletContextLi 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 class DebugListener extends AbstractLifeCycle implements ServletContextLi 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 class DebugListener extends AbstractLifeCycle implements ServletContextLi log("> ctx=%s", cname); else { - String rname = findRequestName(request.getServletApiRequest()); + String rname = findRequestName(request); if (_renameThread) { @@ -316,7 +314,7 @@ public class DebugListener extends AbstractLifeCycle implements ServletContextLi 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 1a1995f17e4..73e880a889f 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 @@ public class DefaultServlet extends HttpServlet 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 class DefaultServlet extends HttpServlet 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 class DefaultServlet extends HttpServlet { 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 4d43b9f9d3f..d1bc1702c1f 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 class Dispatcher implements RequestDispatcher 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 107e2e1f314..93dc82a1c59 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 class ErrorHandler implements Request.Handler 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 @@ public class ErrorHandler implements Request.Handler 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 @@ public class ErrorHandler implements Request.Handler // 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 @@ public class ErrorHandler implements Request.Handler 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 @@ public class ErrorHandler implements Request.Handler 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 531291982af..4d095125700 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 class HttpInput extends ServletInputStream implements Runnable 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 3d1298c5d6c..b5d3548fb9a 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.ByteBufferPool; 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable _commitSize = _bufferSize; } } - - public Response getResponse() - { - return _response; - } public boolean isWritten() { @@ -188,14 +173,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable } } - 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable { 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 class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable if (error != null) { _servletChannel.abort(error); - _writeBlocker.fail(error); _state = State.CLOSED; } else @@ -463,7 +442,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable // 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 class HttpOutput extends ServletOutputStream implements Runnable // 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 class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable { 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 class HttpOutput extends ServletOutputStream implements Runnable 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable { 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable 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 class HttpOutput extends ServletOutputStream implements Runnable */ 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 class HttpOutput extends ServletOutputStream implements Runnable */ 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 class HttpOutput extends ServletOutputStream implements Runnable { 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 class HttpOutput extends ServletOutputStream implements Runnable _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 class HttpOutput extends ServletOutputStream implements Runnable @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 class HttpOutput extends ServletOutputStream implements Runnable 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable 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 @@ public class HttpOutput extends ServletOutputStream implements Runnable { 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 class HttpOutput extends ServletOutputStream implements Runnable } } - 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 1a3de77dc74..7d8eba15712 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 @@ class PushBuilderImpl implements PushBuilder } 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 b12dfe156eb..d1e1d1ab8ad 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.HttpSession; 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.ConnectionMetaData; 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.Logger; 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 @@ public class ServletApiRequest implements HttpServletRequest 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 @@ public class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @Override public Enumeration getHeaderNames() { - return getFields().getFieldNames(); + return Collections.enumeration(getFields().getFieldNamesCollection()); } @Override @@ -292,28 +292,28 @@ public class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest 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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest { 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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest 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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest 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 class ServletApiRequest implements HttpServletRequest sessionId = getRequestedSessionId(); } - return new PushBuilderImpl(_request, pushHeaders, sessionId); + return new PushBuilderImpl(ServletContextRequest.getServletContextRequest(this), pushHeaders, sessionId); } @Override @@ -659,17 +662,17 @@ public class ServletApiRequest implements HttpServletRequest 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 class ServletApiRequest implements HttpServletRequest { 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 class ServletApiRequest implements HttpServletRequest 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 class ServletApiRequest implements HttpServletRequest 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 @@ public class ServletApiRequest implements HttpServletRequest { 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 @@ public class ServletApiRequest implements HttpServletRequest { try { - _contentParameters = FormFields.get(getServletContextRequest()).get(); + _contentParameters = FormFields.get(getRequest()).get(); } catch (IllegalStateException | IllegalArgumentException | ExecutionException | InterruptedException e) @@ -918,14 +908,14 @@ public class ServletApiRequest implements HttpServletRequest // 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 @@ public class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest 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 @@ public class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest private int findServerPort() { - ServletChannel servletChannel = _request.getServletChannel(); + ServletChannel servletChannel = getServletRequestInfo().getServletChannel(); if (servletChannel != null) { HostPort serverAuthority = servletChannel.getServerAuthority(); @@ -1043,9 +1033,9 @@ public class ServletApiRequest implements HttpServletRequest } }; } - 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 @@ public class ServletApiRequest implements HttpServletRequest @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 @@ public class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest { 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 class ServletApiRequest implements HttpServletRequest { 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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 class ServletApiRequest implements HttpServletRequest @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 d7798dd431d..8211dca565f 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 @@ package org.eclipse.jetty.ee10.servlet; 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 java.util.function.Supplier; 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 class ServletApiResponse implements HttpServletResponse 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 class ServletApiResponse implements HttpServletResponse { 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 class ServletApiResponse implements HttpServletResponse { if (isCommitted()) throw new IllegalStateException("Committed"); - _response.getState().sendError(sc, msg); + getServletRequestInfo().getServletRequestState().sendError(sc, msg); } } } @@ -160,7 +188,7 @@ public class ServletApiResponse implements HttpServletResponse 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 class ServletApiResponse implements HttpServletResponse @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 class ServletApiResponse implements HttpServletResponse { // TODO do we need int versions? if (!isCommitted()) - _response.getHeaders().put(name, value); + getResponse().getHeaders().put(name, value); } @Override @@ -202,136 +230,99 @@ public class ServletApiResponse implements HttpServletResponse { // 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 class ServletApiResponse implements HttpServletResponse // 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 class ServletApiResponse implements HttpServletResponse if (contentType == null) { - if (_response.isWriting() && _response.getCharacterEncoding() != null) - throw new IllegalSelectorException(); + if (getServletResponseInfo().isWriting() && getServletResponseInfo().getCharacterEncoding() != null) + throw new IllegalStateException(); - if (_response.getLocale() == null) - _response.setCharacterEncoding(null); - _response.setMimeType(null); - _response.setContentType(null); - _response.getHeaders().remove(HttpHeader.CONTENT_TYPE); + 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 class ServletApiResponse implements HttpServletResponse 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 class ServletApiResponse implements HttpServletResponse { 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 class ServletApiResponse implements HttpServletResponse 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 class ServletApiResponse implements HttpServletResponse 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> getTrailerFields() { - return _response.getTrailers(); + return getServletResponseInfo().getTrailers(); } @Override @@ -536,13 +481,12 @@ public class ServletApiResponse implements HttpServletResponse { if (isCommitted()) throw new IllegalStateException("Committed"); - HttpVersion version = HttpVersion.fromString(_response.getServletContextRequest().getConnectionMetaData().getProtocol()); + HttpVersion version = HttpVersion.fromString(getServletRequestInfo().getRequest().getConnectionMetaData().getProtocol()); if (version == null || version.compareTo(HttpVersion.HTTP_1_1) < 0) throw new IllegalStateException("Trailers not supported in " + version); - _response.setTrailers(trailers); - - _response.setTrailersSupplier(() -> + getServletResponseInfo().setTrailers(trailers); + getResponse().setTrailersSupplier(() -> { Map map = trailers.get(); if (map == null) @@ -556,6 +500,12 @@ public class ServletApiResponse implements HttpServletResponse }); } + @Override + public String toString() + { + return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), getResponse(), getServletResponseInfo()); + } + static class HttpCookieFacade implements HttpCookie { private final Cookie _cookie; @@ -640,7 +590,7 @@ public class ServletApiResponse implements HttpServletResponse @Override public boolean equals(Object obj) { - return HttpCookie.equals(this, obj); + return obj instanceof HttpCookie && HttpCookie.equals(this, obj); } @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java index 6ced9cd2d8b..53c0ea66d2b 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannel.java @@ -16,7 +16,6 @@ package org.eclipse.jetty.ee10.servlet; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; @@ -31,13 +30,15 @@ import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.QuietException; -import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.ConnectionMetaData; import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.ResponseUtils; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextRequest; import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.ExceptionUtil; @@ -56,6 +57,13 @@ import static org.eclipse.jetty.util.thread.Invocable.InvocationType.NON_BLOCKIN * output according to the servlet specification. The combined state so obtained * is reflected in the behaviour of the contained {@link HttpInput} implementation of * {@link jakarta.servlet.ServletInputStream}. + *

+ * This class is reusable over multiple requests for the same {@link ServletContextHandler} + * and is {@link #recycle() recycled} after each use before being + * {@link #associate(ServletContextRequest) associated} with a new {@link ServletContextRequest} + * and then {@link #associate(Request, Response, Callback) associated} with possibly wrapped + * request, response and callback. + *

* * @see ServletRequestState * @see HttpInput @@ -67,35 +75,35 @@ public class ServletChannel private final ServletRequestState _state; private final ServletContextHandler.ServletScopedContext _context; private final ServletContextHandler.ServletContextApi _servletContextApi; + private final ConnectionMetaData _connectionMetaData; private final AtomicLong _requests = new AtomicLong(); - private final Connector _connector; - private final Executor _executor; - private final HttpConfiguration _configuration; - private final EndPoint _endPoint; private final HttpInput _httpInput; - private volatile ServletContextRequest _servletContextRequest; - private volatile boolean _expects100Continue; - private volatile Callback _callback; - // Bytes written after interception (e.g. after compression). - private volatile long _written; + private final HttpOutput _httpOutput; + private ServletContextRequest _servletContextRequest; + private Request _request; + private Response _response; + private Callback _callback; + private boolean _expects100Continue; + private long _written; public ServletChannel(ServletContextHandler servletContextHandler, Request request) { - _state = new ServletRequestState(this); - _context = servletContextHandler.getContext(); - _servletContextApi = _context.getServletContext(); - _connector = request.getConnectionMetaData().getConnector(); - _executor = request.getContext(); - _configuration = request.getConnectionMetaData().getHttpConfiguration(); - _endPoint = request.getConnectionMetaData().getConnection().getEndPoint(); - _httpInput = new HttpInput(this); + this(servletContextHandler, request.getConnectionMetaData()); } - public void setCallback(Callback callback) + public ServletChannel(ServletContextHandler servletContextHandler, ConnectionMetaData connectionMetaData) { - if (_callback != null) - throw new IllegalStateException(); - _callback = callback; + _context = servletContextHandler.getContext(); + _servletContextApi = _context.getServletContext(); + _connectionMetaData = connectionMetaData; + _state = new ServletRequestState(this); + _httpInput = new HttpInput(this); + _httpOutput = new HttpOutput(this); + } + + public ConnectionMetaData getConnectionMetaData() + { + return _connectionMetaData; } public Callback getCallback() @@ -105,14 +113,18 @@ public class ServletChannel /** * Associate this channel with a specific request. - * @param servletContextRequest The request to associate + * This is called by the ServletContextHandler when a core {@link Request} is accepted and associated with + * a servlet mapping. + * @param servletContextRequest The servlet context request to associate * @see #recycle() */ public void associate(ServletContextRequest servletContextRequest) { _state.recycle(); _httpInput.reopen(); - _servletContextRequest = servletContextRequest; + _httpOutput.recycle(); + _request = _servletContextRequest = servletContextRequest; + _response = _servletContextRequest.getServletContextResponse(); _expects100Continue = servletContextRequest.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); if (LOG.isDebugEnabled()) @@ -122,24 +134,49 @@ public class ServletChannel _state); } + /** + * Associate this channel with possibly wrapped values for + * {@link #getRequest()}, {@link #getResponse()} and {@link #getCallback()}. + * This is called by the {@link ServletHandler} immediately before calling {@link #handle()} on the + * initial dispatch. This allows for handlers between the {@link ServletContextHandler} and the + * {@link ServletHandler} to wrap the instances. + * @param request The request, which may have been wrapped + * after #{@link ServletContextHandler#wrapRequest(Request, Response)} + * @param response The response, which may have been wrapped + * after #{@link ServletContextHandler#wrapResponse(ContextRequest, Response)} + * @param callback The context, which may have been wrapped + * after {@link ServletContextHandler#handle(Request, Response, Callback)} + */ + public void associate(Request request, Response response, Callback callback) + { + if (_callback != null) + throw new IllegalStateException(); + + if (request != _request && Request.as(request, ServletContextRequest.class) != _servletContextRequest) + throw new IllegalStateException(); + _request = request; + _response = response; + _callback = callback; + } + public ServletContextHandler.ServletScopedContext getContext() { return _context; } - public ServletContextHandler getContextHandler() + public ServletContextHandler getServletContextHandler() { return _context.getContextHandler(); } - public ServletContextHandler.ServletContextApi getServletContext() + public ServletContextHandler.ServletContextApi getServletContextApi() { return _servletContextApi; } public HttpOutput getHttpOutput() { - return _servletContextRequest.getHttpOutput(); + return _httpOutput; } public HttpInput getHttpInput() @@ -166,7 +203,7 @@ public class ServletChannel return HostPort.normalizeHost(addr); } - public ServletRequestState getState() + public ServletRequestState getServletRequestState() { return _state; } @@ -185,7 +222,7 @@ public class ServletChannel */ public long getIdleTimeout() { - return _endPoint.getIdleTimeout(); + return _connectionMetaData.getConnection().getEndPoint().getIdleTimeout(); } /** @@ -197,38 +234,70 @@ public class ServletChannel */ public void setIdleTimeout(long timeoutMs) { - _endPoint.setIdleTimeout(timeoutMs); + _connectionMetaData.getConnection().getEndPoint().setIdleTimeout(timeoutMs); } public HttpConfiguration getHttpConfiguration() { - return _configuration; + return _connectionMetaData.getHttpConfiguration(); } public Server getServer() { - return _connector.getServer(); + return _context.getContextHandler().getServer(); } + /** + * @return The {@link ServletContextRequest} as wrapped by the {@link ServletContextHandler}. + * @see #getRequest() + */ public ServletContextRequest getServletContextRequest() { return _servletContextRequest; } - public ServletContextResponse getResponse() + /** + * @return The core {@link Request} associated with the request. This may differ from {@link #getServletContextRequest()} + * if the request was wrapped by another handler after the {@link ServletContextHandler} and passed + * to {@link ServletChannel#associate(Request, Response, Callback)}. + * @see #getServletContextRequest() + * @see #associate(Request, Response, Callback) + */ + public Request getRequest() + { + return _request; + } + + /** + * @return The ServetContextResponse as wrapped by the {@link ServletContextHandler}. + * @see #getResponse() + */ + public ServletContextResponse getServletContextResponse() { ServletContextRequest request = _servletContextRequest; - return request == null ? null : request.getResponse(); + return request == null ? null : request.getServletContextResponse(); + } + + /** + * @return The core {@link Response} associated with the API response. + * This may differ from {@link #getServletContextResponse()} if the response was wrapped by another handler + * after the {@link ServletContextHandler} and passed to {@link ServletChannel#associate(Request, Response, Callback)}. + * @see #getServletContextResponse() + * @see #associate(Request, Response, Callback) + */ + public Response getResponse() + { + return _response; } public Connection getConnection() { - return _endPoint.getConnection(); + return _connectionMetaData.getConnection(); } public EndPoint getEndPoint() { - return _endPoint; + return getConnection().getEndPoint(); } /** @@ -306,7 +375,7 @@ public class ServletChannel return ((InetSocketAddress)localAddress); } - SocketAddress local = _endPoint.getLocalSocketAddress(); + SocketAddress local = getEndPoint().getLocalSocketAddress(); if (local instanceof InetSocketAddress) return (InetSocketAddress)local; return null; @@ -314,7 +383,7 @@ public class ServletChannel public InetSocketAddress getRemoteAddress() { - SocketAddress remote = _endPoint.getRemoteSocketAddress(); + SocketAddress remote = getEndPoint().getRemoteSocketAddress(); if (remote instanceof InetSocketAddress) return (InetSocketAddress)remote; return null; @@ -351,7 +420,7 @@ public class ServletChannel throw new IOException("Committed before 100 Continue"); try { - getResponse().writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY).get(); + getServletContextResponse().writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY).get(); } catch (Throwable x) { @@ -373,6 +442,7 @@ public class ServletChannel } /** + * Handle the servlet request. This is called on the initial dispatch and then again on any asynchronous events. * @return True if the channel is ready to continue handling (ie it is not suspended) */ public boolean handle() @@ -413,7 +483,7 @@ public class ServletChannel _context.getServletContextHandler().requestInitialized(_servletContextRequest, _servletContextRequest.getServletApiRequest()); ServletHandler servletHandler = _context.getServletContextHandler().getServletHandler(); - ServletHandler.MappedServlet mappedServlet = _servletContextRequest._mappedServlet; + ServletHandler.MappedServlet mappedServlet = _servletContextRequest.getMatchedResource().getResource(); mappedServlet.handle(servletHandler, Request.getPathInContext(_servletContextRequest), _servletContextRequest.getServletApiRequest(), _servletContextRequest.getHttpServletResponse()); } @@ -465,7 +535,7 @@ public class ServletChannel // We first worked with the core pathInContext above, but now need to convert to servlet style String decodedPathInContext = URIUtil.decodePath(pathInContext); - Dispatcher dispatcher = new Dispatcher(getContextHandler(), uri, decodedPathInContext); + Dispatcher dispatcher = new Dispatcher(getServletContextHandler(), uri, decodedPathInContext); dispatcher.async(asyncContextEvent.getSuppliedRequest(), asyncContextEvent.getSuppliedResponse()); } finally @@ -487,14 +557,14 @@ public class ServletChannel try { // Get ready to send an error response - getResponse().resetContent(); + getServletContextResponse().resetContent(); // the following is needed as you cannot trust the response code and reason // as those could have been modified after calling sendError Integer code = (Integer)_servletContextRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); if (code == null) code = HttpStatus.INTERNAL_SERVER_ERROR_500; - getResponse().setStatus(code); + getServletContextResponse().setStatus(code); // The handling of the original dispatch failed, and we are now going to either generate // and error response ourselves or dispatch for an error page. If there is content left over @@ -502,13 +572,13 @@ public class ServletChannel // Connection:close. This can't be deferred to COMPLETE as the response will be committed // by then. if (!_httpInput.consumeAvailable()) - ResponseUtils.ensureNotPersistent(_servletContextRequest, _servletContextRequest.getResponse()); + ResponseUtils.ensureNotPersistent(_servletContextRequest, _servletContextRequest.getServletContextResponse()); ContextHandler.ScopedContext context = (ContextHandler.ScopedContext)_servletContextRequest.getAttribute(ErrorHandler.ERROR_CONTEXT); Request.Handler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); // If we can't have a body or have no ErrorHandler, then create a minimal error response. - if (HttpStatus.hasNoBody(getResponse().getStatus()) || errorHandler == null) + if (HttpStatus.hasNoBody(getServletContextResponse().getStatus()) || errorHandler == null) { sendResponseAndComplete(); } @@ -521,7 +591,7 @@ public class ServletChannel { // We do not notify ServletRequestListener on this dispatch because it might not // be dispatched to an error page, so we delegate this responsibility to the ErrorHandler. - dispatch(() -> errorHandler.handle(_servletContextRequest, getResponse(), blocker)); + dispatch(() -> errorHandler.handle(_servletContextRequest, getServletContextResponse(), blocker)); blocker.block(); } } @@ -542,7 +612,7 @@ public class ServletChannel { try { - getResponse().resetContent(); + getServletContextResponse().resetContent(); sendResponseAndComplete(); } catch (Throwable t) @@ -580,7 +650,7 @@ public class ServletChannel case COMPLETE: { - if (!getResponse().isCommitted()) + if (!getServletContextResponse().isCommitted()) { /* TODO: isHandled does not exist and HttpOutput might not be explicitly closed. @@ -593,15 +663,15 @@ public class ServletChannel */ // Indicate Connection:close if we can't consume all. - if (getResponse().getStatus() >= 200) - ResponseUtils.ensureConsumeAvailableOrNotPersistent(_servletContextRequest, _servletContextRequest.getResponse()); + if (getServletContextResponse().getStatus() >= 200) + ResponseUtils.ensureConsumeAvailableOrNotPersistent(_servletContextRequest, _servletContextRequest.getServletContextResponse()); } // RFC 7230, section 3.3. if (!_servletContextRequest.isHead() && - getResponse().getStatus() != HttpStatus.NOT_MODIFIED_304 && - !getResponse().isContentComplete(_servletContextRequest.getHttpOutput().getWritten())) + getServletContextResponse().getStatus() != HttpStatus.NOT_MODIFIED_304 && + getServletContextResponse().isContentIncomplete(_servletContextRequest.getHttpOutput().getWritten())) { if (sendErrorOrAbort("Insufficient content written")) break; @@ -613,7 +683,7 @@ public class ServletChannel break; // Set a close callback on the HttpOutput to make it an async callback - getResponse().completeOutput(Callback.from(NON_BLOCKING, () -> _state.completed(null), _state::completed)); + getServletContextResponse().completeOutput(Callback.from(NON_BLOCKING, () -> _state.completed(null), _state::completed)); break; } @@ -654,7 +724,7 @@ public class ServletChannel return false; } - getResponse().getServletApiResponse().sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); + getServletContextResponse().getServletApiResponse().sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); return true; } catch (Throwable x) @@ -667,7 +737,7 @@ public class ServletChannel private void dispatch(Dispatchable dispatchable) throws Exception { - _servletContextRequest.getResponse().getHttpOutput().reopen(); + _servletContextRequest.getServletContextResponse().getHttpOutput().reopen(); getHttpOutput().reopen(); dispatchable.dispatch(); } @@ -749,7 +819,7 @@ public class ServletChannel try { _state.completing(); - getResponse().write(true, getResponse().getHttpOutput().getByteBuffer(), Callback.from(() -> _state.completed(null), _state::completed)); + getServletContextResponse().write(true, getServletContextResponse().getHttpOutput().getByteBuffer(), Callback.from(() -> _state.completed(null), _state::completed)); } catch (Throwable x) { @@ -854,7 +924,7 @@ public class ServletChannel protected void execute(Runnable task) { - _executor.execute(task); + _context.execute(task); } /** diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java index 7bbc574a25f..38c50548bfb 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.nio.charset.Charset; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -35,6 +36,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; @@ -42,7 +44,6 @@ import jakarta.servlet.FilterRegistration; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.Servlet; import jakarta.servlet.ServletContainerInitializer; -import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContextAttributeEvent; import jakarta.servlet.ServletContextAttributeListener; import jakarta.servlet.ServletContextEvent; @@ -66,9 +67,12 @@ import jakarta.servlet.http.HttpSessionAttributeListener; import jakarta.servlet.http.HttpSessionBindingListener; import jakarta.servlet.http.HttpSessionIdListener; import jakarta.servlet.http.HttpSessionListener; +import org.eclipse.jetty.ee10.servlet.ServletContextResponse.EncodingFrom; +import org.eclipse.jetty.ee10.servlet.ServletContextResponse.OutputType; import org.eclipse.jetty.ee10.servlet.security.ConstraintAware; import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping; import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler; +import org.eclipse.jetty.ee10.servlet.writer.ResponseWriter; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.pathmap.MatchedResource; import org.eclipse.jetty.security.SecurityHandler; @@ -80,6 +84,9 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.ContextRequest; import org.eclipse.jetty.server.handler.ContextResponse; +import org.eclipse.jetty.session.AbstractSessionManager; +import org.eclipse.jetty.session.ManagedSession; +import org.eclipse.jetty.session.SessionManager; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.DecoratedObjectFactory; @@ -116,7 +123,7 @@ import static jakarta.servlet.ServletContext.TEMPDIR; * *

* This class should have been called ServletContext, but this would have - * cause confusion with {@link ServletContext}. + * cause confusion with {@link jakarta.servlet.ServletContext}. */ @ManagedObject("Servlet Context Handler") public class ServletContextHandler extends ContextHandler @@ -153,7 +160,7 @@ public class ServletContextHandler extends ContextHandler DESTROYED } - public static ServletContextHandler getServletContextHandler(ServletContext servletContext, String purpose) + public static ServletContextHandler getServletContextHandler(jakarta.servlet.ServletContext servletContext, String purpose) { if (servletContext instanceof ServletContextApi servletContextApi) return servletContextApi.getContext().getServletContextHandler(); @@ -163,7 +170,7 @@ public class ServletContextHandler extends ContextHandler throw new IllegalStateException("No Jetty ServletContextHandler, " + purpose + " unavailable"); } - public static ServletContextHandler getServletContextHandler(ServletContext servletContext) + public static ServletContextHandler getServletContextHandler(jakarta.servlet.ServletContext servletContext) { if (servletContext instanceof ServletContextApi) return ((ServletContextApi)servletContext).getContext().getServletContextHandler(); @@ -171,12 +178,12 @@ public class ServletContextHandler extends ContextHandler return getCurrentServletContextHandler(); } - public static ServletContext getCurrentServletContext() + public static jakarta.servlet.ServletContext getCurrentServletContext() { return getServletContext(ContextHandler.getCurrentContext()); } - public static ServletContext getServletContext(Context context) + public static jakarta.servlet.ServletContext getServletContext(Context context) { if (context instanceof ServletScopedContext) return ((ServletScopedContext)context).getServletContext(); @@ -203,7 +210,6 @@ public class ServletContextHandler extends ContextHandler private Map _localeEncodingMap; private String[] _welcomeFiles; private Logger _logger; - protected boolean _allowNullPathInfo; private int _maxFormKeys = Integer.getInteger(MAX_FORM_KEYS_KEY, DEFAULT_MAX_FORM_KEYS); private int _maxFormContentSize = Integer.getInteger(MAX_FORM_CONTENT_SIZE_KEY, DEFAULT_MAX_FORM_CONTENT_SIZE); private boolean _usingSecurityManager = getSecurityManager() != null; @@ -339,7 +345,7 @@ public class ServletContextHandler extends ContextHandler /** * Get the context path in a form suitable to be returned from {@link HttpServletRequest#getContextPath()} - * or {@link ServletContext#getContextPath()}. + * or {@link jakarta.servlet.ServletContext#getContextPath()}. * * @return Returns the encoded contextPath, or empty string for root context */ @@ -844,7 +850,7 @@ public class ServletContextHandler extends ContextHandler void exitScope(ServletScopedContext context, ServletContextRequest request); } - public ServletContext getServletContext() + public jakarta.servlet.ServletContext getServletContext() { return getContext().getServletContext(); } @@ -1139,8 +1145,13 @@ public class ServletContextHandler extends ContextHandler // Get a servlet request, possibly from a cached version in the channel attributes. Attributes cache = request.getComponents().getCache(); - ServletChannel servletChannel = (ServletChannel)cache.getAttribute(ServletChannel.class.getName()); - if (servletChannel == null || servletChannel.getContext() != getContext()) + Object cachedChannel = cache.getAttribute(ServletChannel.class.getName()); + ServletChannel servletChannel; + if (cachedChannel instanceof ServletChannel sc && sc.getContext() == getContext()) + { + servletChannel = sc; + } + else { servletChannel = new ServletChannel(this, request); cache.setAttribute(ServletChannel.class.getName(), servletChannel); @@ -1155,16 +1166,15 @@ public class ServletContextHandler extends ContextHandler protected ContextResponse wrapResponse(ContextRequest request, Response response) { if (request instanceof ServletContextRequest servletContextRequest) - return servletContextRequest.getResponse(); + return servletContextRequest.getServletContextResponse(); throw new IllegalArgumentException(); } @Override protected boolean handleByContextHandler(String pathInContext, ContextRequest request, Response response, Callback callback) { - ServletContextRequest scopedRequest = Request.as(request, ServletContextRequest.class); - DispatcherType dispatch = scopedRequest.getServletApiRequest().getDispatcherType(); - if (dispatch == DispatcherType.REQUEST && isProtectedTarget(scopedRequest.getDecodedPathInContext())) + boolean initialDispatch = request instanceof ServletContextRequest; + if (initialDispatch && isProtectedTarget(pathInContext)) { Response.writeError(request, response, callback, HttpServletResponse.SC_NOT_FOUND, null); return true; @@ -2033,7 +2043,7 @@ public class ServletContextHandler extends ContextHandler } } - public class ServletContextApi implements ServletContext + public class ServletContextApi implements jakarta.servlet.ServletContext { public static final int SERVLET_MAJOR_VERSION = 6; public static final int SERVLET_MINOR_VERSION = 0; @@ -2695,7 +2705,7 @@ public class ServletContextHandler extends ContextHandler } @Override - public ServletContext getContext(String uripath) + public jakarta.servlet.ServletContext getContext(String uripath) { //TODO jetty-12 does not currently support cross context dispatch return null; @@ -3033,4 +3043,81 @@ public class ServletContextHandler extends ContextHandler super.doStop(); } } + + /** + * The interface used by {@link ServletApiRequest} to access the {@link ServletContextRequest} without + * access to the unwrapped {@link Request} methods. + */ + public interface ServletRequestInfo + { + String getDecodedPathInContext(); + + ManagedSession getManagedSession(); + + Charset getQueryEncoding(); + + default Request getRequest() + { + return getServletChannel().getRequest(); + } + + List getRequestAttributeListeners(); + + ServletScopedContext getServletContext(); + + HttpInput getHttpInput(); + + MatchedResource getMatchedResource(); + + AbstractSessionManager.RequestedSession getRequestedSession(); + + ServletChannel getServletChannel(); + + ServletContextHandler getServletContextHandler(); + + ServletRequestState getServletRequestState(); + + SessionManager getSessionManager(); + + ServletRequestState getState(); + + void setQueryEncoding(String s); + } + + /** + * The interface used by {@link ServletApiResponse} to access the {@link ServletContextResponse} without + * access to the unwrapped {@link Response} methods. + */ + public interface ServletResponseInfo + { + String getCharacterEncoding(boolean setContentType); + + String getCharacterEncoding(); + + String getContentType(); + + EncodingFrom getEncodingFrom(); + + Locale getLocale(); + + OutputType getOutputType(); + + Response getResponse(); + + Supplier> getTrailers(); + + ResponseWriter getWriter(); + + boolean isWriting(); + + void setCharacterEncoding(String encoding, EncodingFrom encodingFrom); + + void setLocale(Locale locale); + + void setOutputType(OutputType outputType); + + void setTrailers(Supplier> trailers); + + void setWriter(ResponseWriter responseWriter); + } } 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 a9144f3f989..b09fc410651 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 @@ -31,9 +31,7 @@ import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.UriCompliance; -import org.eclipse.jetty.http.pathmap.MatchedPath; import org.eclipse.jetty.http.pathmap.MatchedResource; -import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.SecureRequestCustomizer; @@ -45,7 +43,16 @@ import org.eclipse.jetty.session.ManagedSession; import org.eclipse.jetty.session.SessionManager; import org.eclipse.jetty.util.Fields; -public class ServletContextRequest extends ContextRequest +/** + * A core request wrapper that carries the servlet related request state, + * which may be used directly by the associated {@link ServletApiRequest}. + * Non-servlet related state, is used indirectly via {@link ServletChannel#getRequest()} + * which may be a wrapper of this request. + *

+ * This class is single use only. + *

+ */ +public class ServletContextRequest extends ContextRequest implements ServletContextHandler.ServletRequestInfo { public static final String MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig"; static final int INPUT_NONE = 0; @@ -57,20 +64,21 @@ public class ServletContextRequest extends ContextRequest public static ServletContextRequest getServletContextRequest(ServletRequest request) { - if (request instanceof ServletApiRequest) - return ((ServletApiRequest)request).getServletContextRequest(); + if (request instanceof ServletApiRequest servletApiRequest && + servletApiRequest.getServletRequestInfo() instanceof ServletContextRequest servletContextRequest) + return servletContextRequest; - Object channel = request.getAttribute(ServletChannel.class.getName()); - if (channel instanceof ServletChannel) - return ((ServletChannel)channel).getServletContextRequest(); + if (request.getAttribute(ServletChannel.class.getName()) instanceof ServletChannel servletChannel) + return servletChannel.getServletContextRequest(); - while (request instanceof ServletRequestWrapper) + while (request instanceof ServletRequestWrapper wrapper) { - request = ((ServletRequestWrapper)request).getRequest(); - } + request = wrapper.getRequest(); - if (request instanceof ServletApiRequest) - return ((ServletApiRequest)request).getServletContextRequest(); + if (request instanceof ServletApiRequest servletApiRequest && + servletApiRequest.getServletRequestInfo() instanceof ServletContextRequest servletContextRequest) + return servletContextRequest; + } throw new IllegalStateException("could not find %s for %s".formatted(ServletContextRequest.class.getSimpleName(), request)); } @@ -78,13 +86,11 @@ public class ServletContextRequest extends ContextRequest private final List _requestAttributeListeners = new ArrayList<>(); private final ServletApiRequest _servletApiRequest; private final ServletContextResponse _response; - final ServletHandler.MappedServlet _mappedServlet; + private final MatchedResource _matchedResource; private final HttpInput _httpInput; private final String _decodedPathInContext; private final ServletChannel _servletChannel; - private final PathSpec _pathSpec; private final SessionManager _sessionManager; - final MatchedPath _matchedPath; private Charset _queryEncoding; private HttpFields _trailers; private ManagedSession _managedSession; @@ -102,11 +108,9 @@ public class ServletContextRequest extends ContextRequest super(servletContextApi.getContext(), request); _servletChannel = servletChannel; _servletApiRequest = newServletApiRequest(); - _mappedServlet = matchedResource.getResource(); + _matchedResource = matchedResource; _httpInput = _servletChannel.getHttpInput(); _decodedPathInContext = decodedPathInContext; - _pathSpec = matchedResource.getPathSpec(); - _matchedPath = matchedResource.getMatchedPath(); _response = newServletContextResponse(response); _sessionManager = sessionManager; addIdleTimeoutListener(this::onIdleTimeout); @@ -114,7 +118,7 @@ public class ServletContextRequest extends ContextRequest protected ServletApiRequest newServletApiRequest() { - if (getHttpURI().hasViolations() && !getServletChannel().getContextHandler().getServletHandler().isDecodeAmbiguousURIs()) + if (getHttpURI().hasViolations() && !getServletChannel().getServletContextHandler().getServletHandler().isDecodeAmbiguousURIs()) { // TODO we should check if current compliance mode allows all the violations? @@ -135,22 +139,25 @@ public class ServletContextRequest extends ContextRequest private boolean onIdleTimeout(TimeoutException timeout) { - return _servletChannel.getState().onIdleTimeout(timeout); + return _servletChannel.getServletRequestState().onIdleTimeout(timeout); } + @Override + public ServletContextHandler getServletContextHandler() + { + return _servletChannel.getServletContextHandler(); + } + + @Override public String getDecodedPathInContext() { return _decodedPathInContext; } - public PathSpec getPathSpec() + @Override + public MatchedResource getMatchedResource() { - return _pathSpec; - } - - public MatchedPath getMatchedPath() - { - return _matchedPath; + return _matchedResource; } @Override @@ -164,22 +171,24 @@ public class ServletContextRequest extends ContextRequest _trailers = trailers; } + @Override public ServletRequestState getState() { - return _servletChannel.getState(); + return _servletChannel.getServletRequestState(); } - public ServletContextResponse getResponse() + public ServletContextResponse getServletContextResponse() { return _response; } @Override - public ServletContextHandler.ServletScopedContext getContext() + public ServletContextHandler.ServletScopedContext getServletContext() { return (ServletContextHandler.ServletScopedContext)super.getContext(); } + @Override public HttpInput getHttpInput() { return _httpInput; @@ -204,16 +213,18 @@ public class ServletContextRequest extends ContextRequest /** * Set the character encoding used for the query string. This call will effect the return of getQueryString and getParamaters. It must be called before any * getParameter methods. - * + *

* The request attribute "org.eclipse.jetty.server.Request.queryEncoding" may be set as an alternate method of calling setQueryEncoding. * * @param queryEncoding the URI query character encoding */ + @Override public void setQueryEncoding(String queryEncoding) { _queryEncoding = Charset.forName(queryEncoding); } + @Override public Charset getQueryEncoding() { return _queryEncoding; @@ -248,8 +259,9 @@ public class ServletContextRequest extends ContextRequest } /** - * @return The current {@link ContextHandler.ScopedContext context} used for this error handling for this request. If the request is asynchronous, - * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext + * @return The current {@link ContextHandler.ScopedContext context} used for this error handling for this request. + * If the request is asynchronous, then it is the context that called async. Otherwise, it is the last non-null + * context passed to #setContext */ public ServletContextHandler.ServletScopedContext getErrorContext() { @@ -257,12 +269,14 @@ public class ServletContextRequest extends ContextRequest return _servletChannel.getContext(); } - ServletRequestState getServletRequestState() + @Override + public ServletRequestState getServletRequestState() { - return _servletChannel.getState(); + return _servletChannel.getServletRequestState(); } - ServletChannel getServletChannel() + @Override + public ServletChannel getServletChannel() { return _servletChannel; } @@ -274,19 +288,15 @@ public class ServletContextRequest extends ContextRequest public HttpServletResponse getHttpServletResponse() { - return _response.getHttpServletResponse(); - } - - public ServletHandler.MappedServlet getMappedServlet() - { - return _mappedServlet; + return _response.getServletApiResponse(); } public String getServletName() { - return _mappedServlet.getServletHolder().getName(); + return getMatchedResource().getResource().getServletHolder().getName(); } + @Override public List getRequestAttributeListeners() { return _requestAttributeListeners; @@ -318,6 +328,7 @@ public class ServletContextRequest extends ContextRequest return isNoParams; } + @Override public ManagedSession getManagedSession() { return _managedSession; @@ -328,6 +339,7 @@ public class ServletContextRequest extends ContextRequest _managedSession = managedSession; } + @Override public SessionManager getSessionManager() { return _sessionManager; @@ -341,6 +353,7 @@ public class ServletContextRequest extends ContextRequest _managedSession = requestedSession.session(); } + @Override public AbstractSessionManager.RequestedSession getRequestedSession() { return _requestedSession; diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java index f6fb3ad1e91..e98059ec682 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java @@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.eclipse.jetty.ee10.servlet.writer.ResponseWriter; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpHeader; @@ -39,16 +40,22 @@ import org.eclipse.jetty.session.ManagedSession; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.StringUtil; -public class ServletContextResponse extends ContextResponse +/** + * A core response wrapper that carries the servlet related response state, + * which may be used directly by the associated {@link ServletApiResponse}. + * Non servlet related state, is used indirectly via {@link ServletChannel#getResponse()} + * which may be a wrapper of this response. + */ +public class ServletContextResponse extends ContextResponse implements ServletContextHandler.ServletResponseInfo { protected enum OutputType { NONE, STREAM, WRITER } - private final HttpOutput _httpOutput; private final ServletChannel _servletChannel; - private final ServletApiResponse _httpServletResponse; + private final ServletApiResponse _servletApiResponse; + private final HttpFields.Mutable.Wrapper _headers; private String _characterEncoding; private String _contentType; private MimeTypes.Type _mimeType; @@ -61,49 +68,59 @@ public class ServletContextResponse extends ContextResponse public static ServletContextResponse getServletContextResponse(ServletResponse response) { - if (response instanceof ServletApiResponse) - return ((ServletApiResponse)response).getResponse(); + if (response instanceof ServletApiResponse servletApiResponse) + return servletApiResponse.getServletRequestInfo().getServletChannel().getServletContextResponse(); while (response instanceof ServletResponseWrapper) { response = ((ServletResponseWrapper)response).getResponse(); + if (response instanceof ServletApiResponse servletApiResponse) + return servletApiResponse.getServletRequestInfo().getServletChannel().getServletContextResponse(); } - if (response instanceof ServletApiResponse) - return ((ServletApiResponse)response).getResponse(); - throw new IllegalStateException("could not find %s for %s".formatted(ServletContextResponse.class.getSimpleName(), response)); } public ServletContextResponse(ServletChannel servletChannel, ServletContextRequest request, Response response) { super(servletChannel.getContext(), request, response); - _httpOutput = new HttpOutput(response, servletChannel); _servletChannel = servletChannel; - _httpServletResponse = newServletApiResponse(); + _servletApiResponse = newServletApiResponse(); + _headers = new HttpFieldsWrapper(response.getHeaders()); } - protected ResponseWriter getWriter() + @Override + public Response getResponse() + { + return _servletChannel.getResponse(); + } + + @Override + public ResponseWriter getWriter() { return _writer; } - protected void setWriter(ResponseWriter writer) + @Override + public void setWriter(ResponseWriter writer) { _writer = writer; } - protected Locale getLocale() + @Override + public Locale getLocale() { return _locale; } - protected void setLocale(Locale locale) + @Override + public void setLocale(Locale locale) { _locale = locale; } - protected EncodingFrom getEncodingFrom() + @Override + public EncodingFrom getEncodingFrom() { return _encodingFrom; } @@ -113,47 +130,38 @@ public class ServletContextResponse extends ContextResponse return _mimeType; } - protected void setMimeType(MimeTypes.Type mimeType) - { - this._mimeType = mimeType; - } - - protected Supplier> getTrailers() + @Override + public Supplier> getTrailers() { return _trailers; } + @Override public void setTrailers(Supplier> trailers) { this._trailers = trailers; } - protected void setContentType(String contentType) - { - this._contentType = contentType; - } - - protected String getCharacterEncoding() + @Override + public String getCharacterEncoding() { return _characterEncoding; } - protected void setCharacterEncoding(String value) - { - _characterEncoding = value; - } - - protected void setOutputType(OutputType outputType) + @Override + public void setOutputType(OutputType outputType) { _outputType = outputType; } - protected String getContentType() + @Override + public String getContentType() { return _contentType; } - protected OutputType getOutputType() + @Override + public OutputType getOutputType() { return _outputType; } @@ -170,27 +178,22 @@ public class ServletContextResponse extends ContextResponse public HttpOutput getHttpOutput() { - return _httpOutput; + return _servletChannel.getHttpOutput(); } - public ServletRequestState getState() + public ServletRequestState getServletRequestState() { - return _servletChannel.getState(); + return _servletChannel.getServletRequestState(); } - public HttpServletResponse getHttpServletResponse() - { - return _httpServletResponse; - } - public ServletApiResponse getServletApiResponse() { - return _httpServletResponse; + return _servletApiResponse; } public void resetForForward() { - _httpServletResponse.resetBuffer(); + _servletApiResponse.resetBuffer(); _outputType = OutputType.NONE; } @@ -198,7 +201,7 @@ public class ServletContextResponse extends ContextResponse { if (_outputType == OutputType.WRITER) _writer.reopen(); - _httpOutput.reopen(); + getHttpOutput().reopen(); } public void completeOutput(Callback callback) @@ -206,7 +209,7 @@ public class ServletContextResponse extends ContextResponse if (_outputType == OutputType.WRITER) _writer.complete(callback); else - _httpOutput.complete(callback); + getHttpOutput().complete(callback); } public boolean isAllContentWritten(long written) @@ -214,9 +217,9 @@ public class ServletContextResponse extends ContextResponse return (_contentLength >= 0 && written >= _contentLength); } - public boolean isContentComplete(long written) + public boolean isContentIncomplete(long written) { - return (_contentLength < 0 || written >= _contentLength); + return (_contentLength >= 0 && written < _contentLength); } public void setContentLength(int len) @@ -224,6 +227,12 @@ public class ServletContextResponse extends ContextResponse setContentLength((long)len); } + @Override + public HttpFields.Mutable getHeaders() + { + return _headers; + } + public void setContentLength(long len) { // Protect from setting after committed as default handling @@ -234,7 +243,7 @@ public class ServletContextResponse extends ContextResponse if (len > 0) { - long written = _httpOutput.getWritten(); + long written = getHttpOutput().getWritten(); if (written > len) throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written); @@ -254,7 +263,7 @@ public class ServletContextResponse extends ContextResponse } else if (len == 0) { - long written = _httpOutput.getWritten(); + long written = getHttpOutput().getWritten(); if (written > 0) throw new IllegalArgumentException("setContentLength(0) when already written " + written); _contentLength = len; @@ -277,7 +286,7 @@ public class ServletContextResponse extends ContextResponse if (_outputType == OutputType.WRITER) _writer.close(); else - _httpOutput.close(); + getHttpOutput().close(); } @Override @@ -285,7 +294,7 @@ public class ServletContextResponse extends ContextResponse { super.reset(); - _httpServletResponse.resetBuffer(); + _servletApiResponse.resetBuffer(); _outputType = OutputType.NONE; _contentLength = -1; _contentType = null; @@ -324,7 +333,7 @@ public class ServletContextResponse extends ContextResponse HttpSession session = getServletContextRequest().getServletApiRequest().getSession(false); if (session != null && session.isNew()) { - SessionHandler sh = _servletChannel.getContextHandler().getSessionHandler(); + SessionHandler sh = _servletChannel.getServletContextHandler().getSessionHandler(); if (sh != null) { ManagedSession managedSession = SessionHandler.ServletSessionApi.getSession(session); @@ -346,7 +355,7 @@ public class ServletContextResponse extends ContextResponse { if (isCommitted()) throw new IllegalStateException("Committed"); - _httpOutput.resetBuffer(); + getHttpOutput().resetBuffer(); _outputType = OutputType.NONE; _contentLength = -1; _contentType = null; @@ -372,6 +381,7 @@ public class ServletContextResponse extends ContextResponse return _characterEncoding; } + @Override public String getCharacterEncoding(boolean setContentType) { // First try explicit char encoding. @@ -400,7 +410,7 @@ public class ServletContextResponse extends ContextResponse } // Try any default char encoding for the context. - ServletContext context = _servletChannel.getServletContextRequest().getContext().getServletContext(); + ServletContext context = _servletChannel.getServletContextRequest().getServletContext().getServletContext(); if (context != null) { encoding = context.getResponseCharacterEncoding(); @@ -419,26 +429,14 @@ public class ServletContextResponse extends ContextResponse return encoding; } - /** - * Set the Character Encoding and EncodingFrom in the raw, with no manipulation - * of the ContentType value, MimeType value, or headers. - * - * @param encoding the character encoding - * @param from where encoding came from - */ - protected void setRawCharacterEncoding(String encoding, EncodingFrom from) - { - _characterEncoding = encoding; - _encodingFrom = from; - } - /** * Update the Content-Type, MimeType, and headers from the provided Character Encoding and * EncodingFrom. * @param encoding the character encoding * @param from where encoding came from */ - protected void setCharacterEncoding(String encoding, EncodingFrom from) + @Override + public void setCharacterEncoding(String encoding, EncodingFrom from) { if (isWriting() || isCommitted()) return; @@ -483,6 +481,7 @@ public class ServletContextResponse extends ContextResponse } } + @Override public boolean isWriting() { return _outputType == OutputType.WRITER; @@ -530,4 +529,148 @@ public class ServletContextResponse extends ContextResponse */ SET_CHARACTER_ENCODING } + + /** + * Wrapper of the response HttpFields to allow specific values to be intercepted. + */ + private class HttpFieldsWrapper extends HttpFields.Mutable.Wrapper + { + public HttpFieldsWrapper(Mutable fields) + { + super(fields); + } + + @Override + public HttpField onAddField(HttpField field) + { + if (field.getHeader() != null) + { + switch (field.getHeader()) + { + case CONTENT_LENGTH -> + { + if (!isCommitted()) + { + return setContentLength(field); + } + } + case CONTENT_TYPE -> + { + if (!isCommitted()) + { + return setContentType(field); + } + } + } + } + + return super.onAddField(field); + } + + @Override + public boolean onRemoveField(HttpField field) + { + if (field.getHeader() != null) + { + switch (field.getHeader()) + { + case CONTENT_LENGTH -> + { + if (!isCommitted()) + _contentLength = -1; + } + case CONTENT_TYPE -> + { + if (!isCommitted()) + { + if (_locale == null) + _characterEncoding = null; + _contentType = null; + _mimeType = null; + } + } + } + } + return true; + } + + private HttpField setContentLength(HttpField field) + { + long len = field.getLongValue(); + long written = _servletChannel.getHttpOutput().getWritten(); + + if (len > 0 && written > len) + throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written); + if (len == 0 && written > 0) + throw new IllegalArgumentException("setContentLength(0) when already written " + written); + + _contentLength = len; + + if (len > 0 && isAllContentWritten(written)) + { + try + { + closeOutput(); + } + catch (IOException e) + { + throw new RuntimeIOException(e); + } + } + + return field; + } + + private HttpField setContentType(HttpField field) + { + _contentType = field.getValue(); + _mimeType = MimeTypes.CACHE.get(_contentType); + + String charset = MimeTypes.getCharsetFromContentType(_contentType); + if (charset == null && _mimeType != null && _mimeType.isCharsetAssumed()) + charset = _mimeType.getCharsetString(); + + if (charset == null) + { + switch (_encodingFrom) + { + case NOT_SET: + break; + case DEFAULT: + case INFERRED: + case SET_CONTENT_TYPE: + case SET_LOCALE: + case SET_CHARACTER_ENCODING: + { + _contentType = _contentType + ";charset=" + _characterEncoding; + _mimeType = MimeTypes.CACHE.get(_contentType); + field = new HttpField(HttpHeader.CONTENT_TYPE, _contentType); + break; + } + default: + throw new IllegalStateException(_encodingFrom.toString()); + } + } + else if (isWriting() && !charset.equalsIgnoreCase(_characterEncoding)) + { + // too late to change the character encoding; + _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType); + if (_characterEncoding != null && (_mimeType == null || !_mimeType.isCharsetAssumed())) + _contentType = _contentType + ";charset=" + _characterEncoding; + _mimeType = MimeTypes.CACHE.get(_contentType); + field = new HttpField(HttpHeader.CONTENT_TYPE, _contentType); + } + else + { + _characterEncoding = charset; + _encodingFrom = ServletContextResponse.EncodingFrom.SET_CONTENT_TYPE; + } + + if (HttpGenerator.__STRICT || _mimeType == null) + return field; + + _contentType = _mimeType.asString(); + return _mimeType.getContentTypeField(); + } + } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHandler.java index ca60667eb60..05d3382883d 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHandler.java @@ -452,10 +452,15 @@ public class ServletHandler extends Handler.Wrapper @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - // We will always have a ServletScopedRequest and MappedServlet otherwise we will not reach ServletHandler. - ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class); - servletContextRequest.getServletChannel().setCallback(callback); - servletContextRequest.getServletChannel().handle(); + // We will always have a ServletContextRequest as we must be within a ServletContextHandler + ServletChannel servletChannel = Request.get(request, ServletContextRequest.class, ServletContextRequest::getServletChannel); + + if (LOG.isDebugEnabled()) + LOG.debug("handle {} {} {} {}", this, request, response, callback); + + // But request, response and/or callback may have been wrapped after the ServletContextHandler, so update the channel. + servletChannel.associate(request, response, callback); + servletChannel.handle(); return true; } 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 9bfabf448ae..3ea59caec4a 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 @@ -116,10 +116,10 @@ public class ServletMultiPartFormData formData.setMaxMemoryFileSize(config.getFileSizeThreshold()); formData.setMaxFileSize(config.getMaxFileSize()); formData.setMaxLength(config.getMaxRequestSize()); - ConnectionMetaData connectionMetaData = request.getServletContextRequest().getConnectionMetaData(); + ConnectionMetaData connectionMetaData = request.getRequest().getConnectionMetaData(); formData.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize()); - ByteBufferPool byteBufferPool = request.getServletContextRequest().getComponents().getByteBufferPool(); + ByteBufferPool byteBufferPool = request.getRequest().getComponents().getByteBufferPool(); Connection connection = connectionMetaData.getConnection(); int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048; InputStream input = request.getInputStream(); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletRequestState.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletRequestState.java index 0be875eb667..ac636441931 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletRequestState.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletRequestState.java @@ -289,7 +289,7 @@ public class ServletRequestState public boolean isResponseCommitted() { - return _servletChannel.getResponse().isCommitted(); + return _servletChannel.getServletContextResponse().isCommitted(); } public boolean isResponseCompleted() @@ -311,7 +311,7 @@ public class ServletRequestState return false; case OPEN: - _servletChannel.getResponse().setStatus(500); + _servletChannel.getServletContextResponse().setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); _outputState = OutputState.ABORTED; return true; @@ -883,7 +883,7 @@ public class ServletRequestState HttpServletRequest httpServletRequest = servletContextRequest.getServletApiRequest(); final Request request = _servletChannel.getServletContextRequest(); - final Response response = _servletChannel.getResponse(); + final Response response = _servletChannel.getServletContextResponse(); if (message == null) message = HttpStatus.getMessage(code); @@ -915,7 +915,7 @@ public class ServletRequestState request.setAttribute(ERROR_MESSAGE, message); // Set Jetty Specific Attributes. - request.setAttribute(ErrorHandler.ERROR_CONTEXT, servletContextRequest.getContext()); + request.setAttribute(ErrorHandler.ERROR_CONTEXT, servletContextRequest.getServletContext()); request.setAttribute(ErrorHandler.ERROR_MESSAGE, message); request.setAttribute(ErrorHandler.ERROR_STATUS, code); @@ -1153,7 +1153,7 @@ public class ServletRequestState public ServletContextHandler getContextHandler() { - return _servletChannel.getContextHandler(); + return _servletChannel.getServletContextHandler(); } void runInContext(AsyncContextEvent event, Runnable runnable) diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/SessionHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/SessionHandler.java index 84d757dfcaa..3734e3ac7e5 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/SessionHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/SessionHandler.java @@ -656,7 +656,7 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si HttpCookie cookie = access(requestedSession.session(), request.getConnectionMetaData().isSecure()); if (cookie != null) { - ServletContextResponse servletContextResponse = servletContextRequest.getResponse(); + ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse(); Response.replaceCookie(servletContextResponse, cookie); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java index bcc9df039dd..20fd61fffa5 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java @@ -714,7 +714,7 @@ public class AsyncServletTest String response = process("forwarder", "name=orig&one=1", null); assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK")); - _history.forEach(System.err::println); + // _history.forEach(System.err::println); assertThat(_history, contains( "REQUEST /ctx/forwarder/info?name=orig&one=1", diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java index c974917d8c1..f78a37bd8b1 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java @@ -30,7 +30,6 @@ import jakarta.servlet.AsyncContext; import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.ServletRequest; @@ -50,8 +49,9 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; import org.hamcrest.Matchers; 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 org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; @@ -91,10 +91,8 @@ public class GzipHandlerTest private Server _server; private LocalConnector _connector; private GzipHandler gzipHandler; - private ServletContextHandler context; - @BeforeEach - public void init() throws Exception + public void init(boolean gzipInContext) throws Exception { _server = new Server(); _connector = new LocalConnector(_server); @@ -104,10 +102,7 @@ public class GzipHandlerTest gzipHandler.setMinGzipSize(16); gzipHandler.setInflateBufferSize(4096); - context = new ServletContextHandler(gzipHandler, "/ctx"); - - _server.setHandler(gzipHandler); - gzipHandler.setHandler(context); + ServletContextHandler context = new ServletContextHandler("/ctx"); context.addServlet(MicroServlet.class, "/micro"); context.addServlet(MicroChunkedServlet.class, "/microchunked"); context.addServlet(TestServlet.class, "/content"); @@ -120,6 +115,17 @@ public class GzipHandlerTest context.addServlet(BufferServlet.class, "/buffer/*"); context.addFilter(CheckFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + if (gzipInContext) + { + _server.setHandler(context); + context.insertHandler(gzipHandler); + } + else + { + _server.setHandler(gzipHandler); + gzipHandler.setHandler(context); + } + _server.start(); } @@ -219,7 +225,7 @@ public class GzipHandlerTest out.setWriteListener(new WriteListener() { - int count = writes == null ? 1 : Integer.valueOf(writes); + int count = writes == null ? 1 : Integer.parseInt(writes); { response.setContentLength(count * __bytes.length); @@ -233,6 +239,7 @@ public class GzipHandlerTest if (count-- == 0) { out.close(); + context.complete(); break; } @@ -315,13 +322,18 @@ public class GzipHandlerTest @AfterEach public void destroy() throws Exception { - _server.stop(); - _server.join(); + if (_server != null) + { + _server.stop(); + _server.join(); + } } - @Test - public void testNotGzipHandler() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testNotGzipHandler(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -345,9 +357,11 @@ public class GzipHandlerTest assertEquals(__content, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testBlockingResponse() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testBlockingResponse(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -372,9 +386,11 @@ public class GzipHandlerTest assertEquals(__content, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testGzipNotModifiedVaryHeader() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipNotModifiedVaryHeader(boolean gzipInContext) throws Exception { + init(gzipInContext); HttpTester.Request request; HttpTester.Response response; @@ -410,9 +426,11 @@ public class GzipHandlerTest assertThat(response.getCSV("Vary", false), contains("Accept-Encoding")); } - @Test - public void testAsyncResponse() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testAsyncResponse(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -436,9 +454,11 @@ public class GzipHandlerTest assertEquals(__content, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testBufferResponse() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testBufferResponse(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -462,9 +482,11 @@ public class GzipHandlerTest assertEquals(__content, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testAsyncLargeResponse() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testAsyncLargeResponse(boolean gzipInContext) throws Exception { + init(gzipInContext); int writes = 100; // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); @@ -494,9 +516,11 @@ public class GzipHandlerTest } } - @Test - public void testAsyncEmptyResponse() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testAsyncEmptyResponse(boolean gzipInContext) throws Exception { + init(gzipInContext); int writes = 0; _server.getDescendant(GzipHandler.class).setMinGzipSize(0); @@ -517,9 +541,11 @@ public class GzipHandlerTest assertThat(response.getCSV("Vary", false), contains("Accept-Encoding")); } - @Test - public void testGzipHandlerWithMultipleAcceptEncodingHeaders() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipHandlerWithMultipleAcceptEncodingHeaders(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -545,9 +571,11 @@ public class GzipHandlerTest assertEquals(__content, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testGzipNotMicro() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipNotMicro(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -572,9 +600,11 @@ public class GzipHandlerTest assertEquals(__micro, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testGzipNotMicroChunked() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipNotMicroChunked(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -601,9 +631,11 @@ public class GzipHandlerTest assertEquals(__micro, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testETagNotGzipHandler() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testETagNotGzipHandler(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -622,9 +654,11 @@ public class GzipHandlerTest assertThat(response.get("ETag"), is(__contentETag)); } - @Test - public void testETagGzipHandler() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testETagGzipHandler(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -643,9 +677,11 @@ public class GzipHandlerTest assertThat(response.get("ETag"), is(__contentETagGzip)); } - @Test - public void testDeleteETagGzipHandler() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testDeleteETagGzipHandler(boolean gzipInContext) throws Exception { + init(gzipInContext); HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -675,9 +711,11 @@ public class GzipHandlerTest assertThat(response.get("Content-Encoding"), not(equalToIgnoringCase("gzip"))); } - @Test - public void testForwardGzipHandler() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testForwardGzipHandler(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -702,9 +740,11 @@ public class GzipHandlerTest assertEquals(__content, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testIncludeGzipHandler() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testIncludeGzipHandler(boolean gzipInContext) throws Exception { + init(gzipInContext); // generated and parsed test HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; @@ -729,11 +769,14 @@ public class GzipHandlerTest assertEquals(__icontent, testOut.toString(StandardCharsets.UTF_8)); } - @Test - public void testIncludeExcludeGzipHandlerInflate() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testIncludeExcludeGzipHandlerInflate(boolean gzipInContext) throws Exception { - gzipHandler.addExcludedInflationPaths("/ctx/echo/exclude"); - gzipHandler.addIncludedInflationPaths("/ctx/echo/include"); + init(gzipInContext); + String path = gzipInContext ? "/echo" : "/ctx/echo"; + gzipHandler.addExcludedInflationPaths(path + "/exclude"); + gzipHandler.addIncludedInflationPaths(path + "/include"); String message = "hello world"; byte[] gzippedMessage = gzipContent(message); @@ -784,9 +827,11 @@ public class GzipHandlerTest assertThat("Included Paths", Arrays.asList(includedPaths), contains("/foo", "^/bar.*$")); } - @Test - public void testGzipRequest() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipRequest(boolean gzipInContext) throws Exception { + init(gzipInContext); String data = "Hello Nice World! "; for (int i = 0; i < 10; ++i) { @@ -816,9 +861,11 @@ public class GzipHandlerTest assertThat(response.getContent(), is(data)); } - @Test - public void testGzipRequestChunked() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipRequestChunked(boolean gzipInContext) throws Exception { + init(gzipInContext); String data = "Hello Nice World! "; for (int i = 0; i < 10; ++i) { @@ -848,9 +895,11 @@ public class GzipHandlerTest assertThat(response.getContent(), is(data)); } - @Test - public void testGzipFormRequest() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipFormRequest(boolean gzipInContext) throws Exception { + init(gzipInContext); String data = "name=value"; ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPOutputStream output = new GZIPOutputStream(baos); @@ -876,9 +925,11 @@ public class GzipHandlerTest assertThat(response.getContent(), is("name: value\n")); } - @Test - public void testGzipBomb() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipBomb(boolean gzipInContext) throws Exception { + init(gzipInContext); byte[] data = new byte[512 * 1024]; Arrays.fill(data, (byte)'X'); @@ -907,9 +958,11 @@ public class GzipHandlerTest assertThat(response.getContentBytes().length, is(512 * 1024)); } - @Test - public void testGzipExcludeNewMimeType() throws Exception + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGzipExcludeNewMimeType(boolean gzipInContext) throws Exception { + init(gzipInContext); // setting all excluded mime-types to a mimetype new mime-type // Note: this mime-type does not exist in MimeTypes object. gzipHandler.setExcludedMimeTypes("image/webfoo"); @@ -955,11 +1008,6 @@ public class GzipHandlerTest public static class CheckFilter implements Filter { - @Override - public void init(FilterConfig filterConfig) throws ServletException - { - } - @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -969,10 +1017,5 @@ public class GzipHandlerTest assertThat(request.getParameter("X-Content-Encoding"), Matchers.nullValue()); chain.doFilter(request, response); } - - @Override - public void destroy() - { - } } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java index 5fe1b30719b..eda08b82516 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java @@ -2358,9 +2358,9 @@ public class ServletContextHandlerTest static class CookieTweakResponseApi extends ServletApiResponse { - CookieTweakResponseApi(ServletApiResponse response) + CookieTweakResponseApi(ServletContextResponse response) { - super(response.getResponse()); + super(response); } @Override @@ -2397,8 +2397,7 @@ public class ServletContextHandlerTest @Override protected ServletApiResponse newServletApiResponse() { - ServletApiResponse servletApiResponse = super.newServletApiResponse(); - return new CookieTweakResponseApi(super.newServletApiResponse()); + return new CookieTweakResponseApi(this); } }; } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletHandlerTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletHandlerTest.java index 98fe2dae5f8..e681ac47de1 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletHandlerTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletHandlerTest.java @@ -767,7 +767,6 @@ public class ServletHandlerTest server.addConnector(connector); server.start(); - server.dumpStdErr(); assertThat(connector.getResponse("GET /default HTTP/1.0\r\n\r\n"), containsString("mapping='/'")); assertThat(connector.getResponse("GET /foo HTTP/1.0\r\n\r\n"), containsString("mapping='/foo'")); diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java index c2ba230c2c0..67c5bfb08e4 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/GzipWithSendErrorTest.java @@ -40,7 +40,6 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.Response; import org.eclipse.jetty.ee10.servlet.HttpInput; import org.eclipse.jetty.ee10.servlet.ListenerHolder; -import org.eclipse.jetty.ee10.servlet.ServletApiRequest; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.eclipse.jetty.http.HttpHeader; @@ -102,7 +101,7 @@ public class GzipWithSendErrorTest { if (onComplete != null) { - ServletContextRequest servletContextRequest = ((ServletApiRequest)sre.getServletRequest()).getServletContextRequest(); + ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(sre.getServletRequest()); onComplete.accept(servletContextRequest); } } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/resources/add-jetty-test-webapp.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/resources/add-jetty-test-webapp.xml index 1196a26b630..c2cbdf90728 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/resources/add-jetty-test-webapp.xml +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/resources/add-jetty-test-webapp.xml @@ -3,16 +3,14 @@ - - /test-jetty-webapp - /test-jetty-webapp.war - - - - 1024 - - - + + 1024 + + + /test-jetty-webapp + /test-jetty-webapp.war + + diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java index 60ec7aaecd2..b216575f4a0 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java @@ -31,7 +31,6 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import jakarta.servlet.ServletContext; import jakarta.servlet.ServletRegistration.Dynamic; import jakarta.servlet.ServletSecurityElement; import jakarta.servlet.http.HttpSessionActivationListener; @@ -1343,11 +1342,10 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL public class ServletApiContext extends ServletContextHandler.ServletContextApi { - @Override - public ServletContext getContext(String uripath) + public jakarta.servlet.ServletContext getContext(String uripath) { - ServletContext servletContext = super.getContext(uripath); + jakarta.servlet.ServletContext servletContext = super.getContext(uripath); if (servletContext != null && _contextWhiteList != null) { diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/server/JakartaWebSocketServerContainer.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/server/JakartaWebSocketServerContainer.java index 787ad0897e3..ecb5827195d 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/server/JakartaWebSocketServerContainer.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/server/JakartaWebSocketServerContainer.java @@ -303,7 +303,7 @@ public class JakartaWebSocketServerContainer extends JakartaWebSocketClientConta Handshaker handshaker = webSocketMappings.getHandshaker(); ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(request); - ServletContextResponse servletContextResponse = servletContextRequest.getResponse(); + ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse(); FutureCallback callback = new FutureCallback(); try diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java index 90391e09030..6b1eb972c84 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java @@ -215,7 +215,7 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements }; ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(request); - ServletContextResponse servletContextResponse = servletContextRequest.getResponse(); + ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse(); WebSocketNegotiator negotiator = WebSocketNegotiator.from(coreCreator, frameHandlerFactory); Handshaker handshaker = webSocketMappings.getHandshaker(); diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServlet.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServlet.java index 6567874ac42..bf9b5366fc0 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServlet.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServlet.java @@ -183,7 +183,7 @@ public abstract class JettyWebSocketServlet extends HttpServlet throws ServletException, IOException { ServletContextRequest request = ServletContextRequest.getServletContextRequest(req); - ServletContextResponse response = request.getResponse(); + ServletContextResponse response = request.getServletContextResponse(); // Do preliminary check before proceeding to attempt an upgrade. if (mapping.getHandshaker().isWebSocketUpgradeRequest(request)) diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-servlet/src/main/java/org/eclipse/jetty/ee10/websocket/servlet/WebSocketUpgradeFilter.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-servlet/src/main/java/org/eclipse/jetty/ee10/websocket/servlet/WebSocketUpgradeFilter.java index 9ae92551a05..3aef96b507f 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-servlet/src/main/java/org/eclipse/jetty/ee10/websocket/servlet/WebSocketUpgradeFilter.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-servlet/src/main/java/org/eclipse/jetty/ee10/websocket/servlet/WebSocketUpgradeFilter.java @@ -158,7 +158,7 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(request); - ServletContextResponse servletContextResponse = servletContextRequest.getResponse(); + ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse(); // Do preliminary check before proceeding to attempt an upgrade. if (mappings.getHandshaker().isWebSocketUpgradeRequest(servletContextRequest)) diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java index d87c9e3fe60..5113c845ff2 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java @@ -23,7 +23,6 @@ import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; -import java.nio.file.Files; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; @@ -1325,7 +1324,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable try { - ReadableByteChannel rbc = Files.newByteChannel(httpContent.getResource().getPath()); + ReadableByteChannel rbc = httpContent.getResource().newReadableByteChannel(); sendContent(rbc, callback); } catch (Throwable x) diff --git a/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java b/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java index 03131097deb..9d03e7f3eb0 100644 --- a/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java +++ b/jetty-ee9/jetty-ee9-servlets/src/test/java/org/eclipse/jetty/ee9/servlets/CloseableDoSFilterTest.java @@ -13,8 +13,6 @@ package org.eclipse.jetty.ee9.servlets; -import java.nio.file.Path; - import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.junit.jupiter.api.BeforeEach; diff --git a/jetty-home/src/main/resources/modules/demo-root.mod b/jetty-home/src/main/resources/modules/demo-root.mod index fc7e4030b8d..de2803b50ba 100644 --- a/jetty-home/src/main/resources/modules/demo-root.mod +++ b/jetty-home/src/main/resources/modules/demo-root.mod @@ -18,5 +18,6 @@ webapps/root/ webapps/root/images/ basehome:modules/demo.d/root/index.html|webapps/root/index.html basehome:modules/demo.d/root/jetty.css|webapps/root/jetty.css +basehome:modules/demo.d/root/favicon.ico|webapps/root/favicon.ico basehome:modules/demo.d/root/images/jetty-pic.png|webapps/root/images/jetty-pic.png basehome:modules/demo.d/root/images/webtide_logo.jpg|webapps/root/images/webtide_logo.jpg diff --git a/jetty-home/src/main/resources/modules/demo.d/root/favicon.ico b/jetty-home/src/main/resources/modules/demo.d/root/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ea9e174b48b0130d4294813a6b5d487f35b436cc GIT binary patch literal 1150 zcmbtT*-leY6um|h48jdk3$&%Q(B4i^j0|O{SS`gNpioe>$}E$@lZJ1QNB+PF^2Rq1 z-~0si(Ki_^w7s|RV6t5MRE#FRd7IU<_sLm%SZ5ziJI7yDmBxRswtPX;YBWs?5!~WV zX`jnKzth8Su;vVy6F!(zey)BLp7>CB>W4KOr0qw3w28if>`f;Q+U+p8Z%!Bx?#XHx z){FsbAqZ=}v|o5k{<>)7Pyike?g%;$(&VSpzKYFT`wR zDrX^M;CRr3qk($tMJ~gfG?=p)=1f4>OZ>eMmFKZ z{#6_645KLgIrf(Ep}0){MOd9rM%JLm7=wuWGDy2FVWh5HVfe~LjL`pd&kbuajQk^N z@{D>BMfSbkW<%5whu%d#k%;ZURdyb!Ya7ZI-%;ttJJuxJLz0cWW_*%td~zN%2=2D-ky` z(BtG2iSC_JOXHW6$^Qk_6M1L8#NWE%;LLH} zWIj0`Qp53(2ibm?;-@;O`5*Ztr|<+(CpGA+Q$34j#g!J$e~9zt#$j(AUOLK=Y~>vE zu?9lyH@sWXP4aHWYq5K$p7{f?rUNjigPh|g$c%mOb6~yGu6q2P&lSgd%&yLxlcl$% z^3Jn=&e49e4f|0W4m)dTSK~8hVtc3o?+0Ai>UUzBcl=?*!x|+Q-zjIsoMPQ_f7DOj qU^eMe?`tnuMcawfS5dwZz6;KEaVb>dF}Ls}x8hTO;ZooK`hEgZQ79q+ literal 0 HcmV?d00001 diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java index 345f792dea8..3f81cdf6881 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -579,4 +580,65 @@ public class DemoModulesTests extends AbstractJettyHomeTest } } } + + @ParameterizedTest + @MethodSource("provideEnvironmentsToTest") + public void testStaticContent(String env) throws Exception + { + Path jettyBase = newTestJettyBaseDirectory(); + String jettyVersion = System.getProperty("jettyVersion"); + JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(jettyBase) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + int httpPort = distribution.freePort(); + + String[] argsConfig = + { + "--add-modules=http," + toEnvironment("demos", env) + }; + + try (JettyHomeTester.Run runConfig = distribution.start(argsConfig)) + { + assertTrue(runConfig.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); + assertEquals(0, runConfig.getExitValue()); + + String[] argsStart = + { + "jetty.http.port=" + httpPort + }; + try (JettyHomeTester.Run runStart = distribution.start(argsStart)) + { + assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + startHttpClient(); + + String rootURI = "http://localhost:%d".formatted(httpPort); + String demoJettyURI = "%s/%s-test".formatted(rootURI, env); + + ContentResponse response; + + for (String welcome : new String[] {"", "/", "/index.html"}) + { + response = client.GET(rootURI + welcome); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("Welcome to Jetty 12")); + + response = client.GET(demoJettyURI + welcome); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("Eclipse Jetty Demo Webapp")); + } + + for (String directory : new String[] {rootURI + "/", demoJettyURI + "/", demoJettyURI + "/rewrite/"}) + { + response = client.GET(directory + "jetty-dir.css"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + response = client.GET(rootURI + "/favicon.ico"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } + } } From 9a38e3ba41283950aaef240fa5986c220d9b0b66 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 23 Jun 2023 07:09:58 +0200 Subject: [PATCH 27/63] Reinstated StopTest (#9950) Reenbled StopTest for #9949 --- .../org/eclipse/jetty/server/StopTest.java | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java index 17f1bf4eb81..3b2ff2d4519 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java @@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.LocalConnector.LocalEndPoint; import org.eclipse.jetty.server.handler.ContextHandler; @@ -39,7 +40,6 @@ import org.eclipse.jetty.util.NanoTime; import org.eclipse.jetty.util.component.LifeCycle; import org.hamcrest.Matcher; import org.hamcrest.Matchers; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +57,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -@Disabled // TODO public class StopTest { private static final Logger LOG = LoggerFactory.getLogger(StopTest.class); @@ -288,31 +287,28 @@ public class StopTest Exchanger exchanger0 = new Exchanger<>(); Exchanger exchanger1 = new Exchanger<>(); - /* TODO - context.setHandler(new AbstractHandler() + context.setHandler(new Handler.Abstract() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException + public boolean handle(Request request, Response response, Callback callback) throws Exception { try { exchanger0.exchange(null); exchanger1.exchange(null); + + response.setStatus(200); + Content.Sink.write(response, true, "The Response", callback); } catch (Throwable x) { - throw new ServletException(x); + callback.failed(x); } - baseRequest.setHandled(true); - response.setStatus(200); - response.getWriter().println("The Response"); - response.getWriter().close(); + return true; } }); - */ server.setStopTimeout(1000); server.start(); @@ -381,32 +377,27 @@ public class StopTest Exchanger exchanger0 = new Exchanger<>(); Exchanger exchanger1 = new Exchanger<>(); - /* TODO - stats.setHandler(new AbstractHandler() + stats.setHandler(new Handler.Abstract() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException + public boolean handle(Request request, Response response, Callback callback) throws Exception { try { exchanger0.exchange(null); exchanger1.exchange(null); + + response.setStatus(200); + Content.Sink.write(response, true, "The Response", callback); } catch (Throwable x) { - throw new ServletException(x); + callback.failed(x); } - - baseRequest.setHandled(true); - response.setStatus(200); - response.getWriter().println("The Response"); - response.getWriter().close(); + return true; } }); - */ - server.start(); LocalEndPoint endp = connector.executeRequest( @@ -535,30 +526,19 @@ public class StopTest public boolean handle(Request request, Response response, Callback callback) throws Exception { response.getHeaders().put(HttpHeader.CONTENT_LENGTH, 2); - response.write(true, ByteBuffer.wrap("a".getBytes()), new Callback() + request.getContext().run(() -> { - @Override - public void succeeded() + try { - try - { - latchA.countDown(); - latchB.await(); - } - catch (InterruptedException e) - { - throw new RuntimeException(e); - } - response.write(true, ByteBuffer.wrap("b".getBytes()), callback); + latchA.countDown(); + latchB.await(); } - - @Override - public void failed(Throwable x) + catch (InterruptedException e) { - callback.failed(x); + throw new RuntimeException(e); } + response.write(true, ByteBuffer.wrap("ab".getBytes()), callback); }); - return true; } } From a3e82326cfadd28f4822926a13b84ec4b03229a7 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 23 Jun 2023 09:17:15 +0200 Subject: [PATCH 28/63] Experiment/jetty 12 chunk isError and warnings (#9904) * Remove usage of instanceof Content.Chunk.Error * Updated AsyncContent to accept a transient failures * Updated AsyncContent to accept a transient failure with inputstream --- .../jetty/docs/programming/ContentDocs.java | 153 ++++++++++++++ .../org/eclipse/jetty/client/Response.java | 4 +- .../jetty/client/transport/HttpReceiver.java | 4 +- .../jetty/client/transport/HttpSender.java | 4 +- .../client/transport/ResponseListeners.java | 2 +- .../jetty/client/ConnectionPoolTest.java | 4 +- .../client/HttpClientAsyncContentTest.java | 5 +- .../server/internal/HttpStreamOverFCGI.java | 2 +- .../org/eclipse/jetty/http/MultiPart.java | 4 +- .../jetty/http/MultiPartByteRanges.java | 4 +- .../eclipse/jetty/http/MultiPartFormData.java | 4 +- .../jetty/http2/tests/IdleTimeoutTest.java | 4 +- .../java/org/eclipse/jetty/io/Content.java | 196 ++++++++++++------ .../jetty/io/content/AsyncContent.java | 60 ++++-- .../io/content/ContentSourceInputStream.java | 12 +- .../io/content/ContentSourcePublisher.java | 4 +- .../io/content/ContentSourceTransformer.java | 9 +- .../io/content/InputStreamContentSource.java | 2 +- .../jetty/io/content/PathContentSource.java | 2 +- .../jetty/io/internal/ContentCopier.java | 15 +- .../io/internal/ContentSourceByteBuffer.java | 4 +- .../io/internal/ContentSourceConsumer.java | 4 +- .../io/internal/ContentSourceString.java | 4 +- .../eclipse/jetty/io/AsyncContentTest.java | 13 +- .../eclipse/jetty/io/ContentSourceTest.java | 71 ++++++- .../io/ContentSourceTransformerTest.java | 13 +- .../org/eclipse/jetty/server/FormFields.java | 4 +- .../org/eclipse/jetty/server/HttpStream.java | 10 +- .../org/eclipse/jetty/server/Request.java | 6 +- .../jetty/server/handler/DelayedHandler.java | 4 +- .../jetty/server/handler/EventsHandler.java | 2 +- .../server/handler/StatisticsHandler.java | 2 +- .../server/handler/gzip/GzipRequest.java | 2 +- .../server/internal/HttpChannelState.java | 16 +- .../jetty/server/internal/HttpConnection.java | 4 +- .../jetty/server/ConnectorTimeoutTest.java | 4 +- .../jetty/server/GracefulHandlerTest.java | 4 +- .../eclipse/jetty/server/HttpChannelTest.java | 5 +- .../jetty/server/HttpServerTestBase.java | 4 +- .../jetty/server/handler/DumpHandler.java | 4 +- .../server/handler/StatisticsHandlerTest.java | 4 +- .../handler/ThreadLimitHandlerTest.java | 4 +- .../test/client/transport/HttpClientTest.java | 9 +- .../client/transport/ServerTimeoutsTest.java | 4 +- .../jetty/ee10/proxy/ProxyServlet.java | 4 +- .../ee10/servlet/AsyncContentProducer.java | 8 +- .../eclipse/jetty/ee10/servlet/HttpInput.java | 20 +- .../eclipse/jetty/ee9/nested/HttpInput.java | 4 +- .../eclipse/jetty/ee9/proxy/ProxyServlet.java | 4 +- .../websocket/tests/ClientDisconnectTest.java | 1 - 50 files changed, 525 insertions(+), 211 deletions(-) create mode 100644 documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java new file mode 100644 index 00000000000..aa3364180f5 --- /dev/null +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java @@ -0,0 +1,153 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.content.AsyncContent; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FutureCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings("unused") +public class ContentDocs +{ + private static final Logger LOG = LoggerFactory.getLogger(ContentDocs.class); + + // tag::echo[] + public void echo(Content.Source source, Content.Sink sink, Callback callback) + { + Callback echo = new Callback() + { + private Content.Chunk chunk; + + public void succeeded() + { + // release any previous chunk + if (chunk != null) + { + chunk.release(); + // complete if it was the last + if (chunk.isLast()) + { + callback.succeeded(); + return; + } + } + + while (true) + { + // read the next chunk + chunk = source.read(); + + if (chunk == null) + { + // if no chunk, demand more and call succeeded when demand is met. + source.demand(this::succeeded); + return; + } + + if (Content.Chunk.isFailure(chunk, true)) + { + // if it is a persistent failure, then fail the callback + callback.failed(chunk.getFailure()); + return; + } + + if (chunk.hasRemaining() || chunk.isLast()) + { + // if chunk has content or is last, write it to the sink and resume this loop in callback + sink.write(chunk.isLast(), chunk.getByteBuffer(), this); + return; + } + + chunk.release(); + } + } + + public void failed(Throwable x) + { + source.fail(x); + callback.failed(x); + } + }; + source.demand(echo::succeeded); + } + // tag::echo[] + + public static void testEcho() throws Exception + { + AsyncContent source = new AsyncContent(); + AsyncContent sink = new AsyncContent(); + + Callback.Completable echoCallback = new Callback.Completable(); + new ContentDocs().echo(source, sink, echoCallback); + + Content.Chunk chunk = sink.read(); + if (chunk != null) + throw new IllegalStateException("No chunk expected yet"); + + FutureCallback onContentAvailable = new FutureCallback(); + sink.demand(onContentAvailable::succeeded); + if (onContentAvailable.isDone()) + throw new IllegalStateException("No demand expected yet"); + + Callback.Completable writeCallback = new Callback.Completable(); + Content.Sink.write(source, false, "One", writeCallback); + if (writeCallback.isDone()) + throw new IllegalStateException("Should wait until first chunk is consumed"); + + onContentAvailable.get(); + chunk = sink.read(); + if (!"One".equals(BufferUtil.toString(chunk.getByteBuffer()))) + throw new IllegalStateException("first chunk is expected"); + + if (writeCallback.isDone()) + throw new IllegalStateException("Should wait until first chunk is consumed"); + chunk.release(); + writeCallback.get(); + + + writeCallback = new Callback.Completable(); + Content.Sink.write(source, true, "Two", writeCallback); + if (writeCallback.isDone()) + throw new IllegalStateException("Should wait until second chunk is consumed"); + + onContentAvailable = new FutureCallback(); + sink.demand(onContentAvailable::succeeded); + if (!onContentAvailable.isDone()) + throw new IllegalStateException("Demand expected for second chunk"); + + chunk = sink.read(); + if (!"Two".equals(BufferUtil.toString(chunk.getByteBuffer()))) + throw new IllegalStateException("second chunk is expected"); + chunk.release(); + writeCallback.get(); + + onContentAvailable = new FutureCallback(); + sink.demand(onContentAvailable::succeeded); + if (!onContentAvailable.isDone()) + throw new IllegalStateException("Demand expected for EOF"); + + chunk = sink.read(); + if (!chunk.isLast()) + throw new IllegalStateException("EOF expected"); + } + + public static void main(String... args) throws Exception + { + testEcho(); + } +} diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Response.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Response.java index 44e30478691..7b2305d7ed9 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Response.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Response.java @@ -175,9 +175,9 @@ public interface Response contentSource.demand(demandCallback); return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - response.abort(error.getCause()); + response.abort(chunk.getFailure()); return; } if (chunk.isLast() && !chunk.hasRemaining()) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java index 07f0e612093..66c1a9dfc0d 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java @@ -554,7 +554,7 @@ public abstract class HttpReceiver _chunk = inputChunk; if (_chunk == null) return null; - if (_chunk instanceof Content.Chunk.Error) + if (Content.Chunk.isFailure(_chunk)) return _chunk; // Retain the input chunk because its ByteBuffer will be referenced by the Inflater. @@ -748,7 +748,7 @@ public abstract class HttpReceiver LOG.debug("Erroring {}", this); try (AutoLock ignored = lock.lock()) { - if (currentChunk instanceof Content.Chunk.Error) + if (Content.Chunk.isFailure(currentChunk)) return false; if (currentChunk != null) currentChunk.release(); diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java index 533d44c3232..328ee0df920 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java @@ -503,8 +503,8 @@ public abstract class HttpSender } } - if (chunk instanceof Content.Chunk.Error error) - throw error.getCause(); + if (Content.Chunk.isFailure(chunk)) + throw chunk.getFailure(); ByteBuffer buffer = chunk.getByteBuffer(); contentBuffer = buffer.asReadOnlyBuffer(); diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/ResponseListeners.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/ResponseListeners.java index a32150dadd2..844727d4ca6 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/ResponseListeners.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/ResponseListeners.java @@ -676,7 +676,7 @@ public class ResponseListeners Content.Chunk currentChunk = chunk; if (LOG.isDebugEnabled()) LOG.debug("Content source #{} fail while current chunk is {}", index, currentChunk); - if (currentChunk instanceof Content.Chunk.Error) + if (Content.Chunk.isFailure(currentChunk)) return; if (currentChunk != null) currentChunk.release(); diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java index 1fc43ea1a69..835c6ba2eec 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java @@ -176,8 +176,8 @@ public class ConnectionPoolTest continue; } } - if (chunk instanceof Content.Chunk.Error error) - throw error.getCause(); + if (Content.Chunk.isFailure(chunk)) + throw chunk.getFailure(); if (chunk.hasRemaining()) { diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java index 15cb660d64f..73b217a17ce 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java @@ -30,7 +30,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; public class HttpClientAsyncContentTest extends AbstractHttpClientServerTest @@ -248,11 +247,11 @@ public class HttpClientAsyncContentTest extends AbstractHttpClientServerTest .onResponseContentSource((response, contentSource) -> response.abort(new Throwable()).whenComplete((failed, x) -> { Content.Chunk chunk = contentSource.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); contentSource.demand(() -> { Content.Chunk c = contentSource.read(); - assertInstanceOf(Content.Chunk.Error.class, c); + assertTrue(Content.Chunk.isFailure(c, true)); errorContentLatch.countDown(); }); })) diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/HttpStreamOverFCGI.java b/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/HttpStreamOverFCGI.java index 74e37e715f3..ba8b07f62dc 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/HttpStreamOverFCGI.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/HttpStreamOverFCGI.java @@ -197,7 +197,7 @@ public class HttpStreamOverFCGI implements HttpStream { if (_chunk == null) _chunk = Content.Chunk.EOF; - else if (!_chunk.isLast() && !(_chunk instanceof Content.Chunk.Error)) + else if (!_chunk.isLast() && !(Content.Chunk.isFailure(_chunk))) throw new IllegalStateException(); } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java index 88d5ac9c390..14ec49ea3fd 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java @@ -563,7 +563,7 @@ public class MultiPart private State state = State.FIRST; private boolean closed; private Runnable demand; - private Content.Chunk.Error errorChunk; + private Content.Chunk errorChunk; private Part part; public AbstractContentSource(String boundary) @@ -759,7 +759,7 @@ public class MultiPart case CONTENT -> { Content.Chunk chunk = part.getContentSource().read(); - if (chunk == null || chunk instanceof Content.Chunk.Error) + if (chunk == null || Content.Chunk.isFailure(chunk)) yield chunk; if (!chunk.isLast()) yield chunk; diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java index c6a203d3035..944d9f1dee5 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java @@ -101,9 +101,9 @@ public class MultiPartByteRanges extends CompletableFutureCopies the given content source to the given content sink, notifying * the given callback when the copy is complete (either succeeded or failed).

- *

In case of failures, the content source is {@link Source#fail(Throwable) failed} - * too.

+ *

In case of {@link Chunk#getFailure() failure chunks}, + * the content source is {@link Source#fail(Throwable) failed} if the failure + * chunk is {@link Chunk#isLast() last}, else the failing is transient and is ignored.

* * @param source the source to copy from * @param sink the sink to copy to @@ -76,6 +77,9 @@ public class Content *

If the predicate returns {@code false}, it means that the chunk is not * handled, its callback will not be completed, and the implementation will * handle the chunk and its callback.

+ *

In case of {@link Chunk#getFailure() failure chunks} not handled by any {@code chunkHandler}, + * the content source is {@link Source#fail(Throwable) failed} if the failure + * chunk is {@link Chunk#isLast() last}, else the failure is transient and is ignored.

* * @param source the source to copy from * @param sink the sink to copy to @@ -103,10 +107,11 @@ public class Content * return; * } * - * // The chunk is an error. - * if (chunk instanceof Chunk.Error error) { - * // Handle the error. - * Throwable cause = error.getCause(); + * // The chunk is a failure. + * if (Content.Chunk.isFailure(chunk)) { + * // Handle the failure. + * Throwable cause = chunk.getFailure(); + * boolean transient = !chunk.isLast(); * // ... * return; * } @@ -190,7 +195,7 @@ public class Content * @return the String obtained from the content * @throws IOException if reading the content fails */ - public static String asString(Source source, Charset charset) throws IOException + static String asString(Source source, Charset charset) throws IOException { try { @@ -227,12 +232,12 @@ public class Content } /** - *

Reads, non-blocking, the given content source, until either an error or EOF, + *

Reads, non-blocking, the given content source, until a {@link Chunk#isFailure(Chunk) failure} or EOF * and discards the content.

* * @param source the source to read from * @param callback the callback to notify when the whole content has been read - * or an error occurred while reading the content + * or a failure occurred while reading the content */ static void consumeAll(Source source, Callback callback) { @@ -240,7 +245,7 @@ public class Content } /** - *

Reads, blocking if necessary, the given content source, until either an error + *

Reads, blocking if necessary, the given content source, until a {@link Chunk#isFailure(Chunk) failure} * or EOF, and discards the content.

* * @param source the source to read from @@ -274,13 +279,15 @@ public class Content *

The returned chunk could be:

*
    *
  • {@code null}, to signal that there isn't a chunk of content available
  • - *
  • an {@link Chunk.Error error} instance, to signal that there was an error + *
  • an {@link Chunk} instance with non null {@link Chunk#getFailure()}, to signal that there was a failure * trying to produce a chunk of content, or that the content production has been * {@link #fail(Throwable) failed} externally
  • *
  • a {@link Chunk} instance, containing the chunk of content.
  • *
- *

Once a read returns an {@link Chunk.Error error} instance, further reads - * will continue to return the same error instance.

+ *

Once a read returns an {@link Chunk} instance with non-null {@link Chunk#getFailure()} + * then if the failure is {@link Chunk#isLast() last} further reads + * will continue to return the same failure chunk instance, otherwise further + * {@code read()} operations may return different non-failure chunks.

*

Once a read returns a {@link Chunk#isLast() last chunk}, further reads will * continue to return a last chunk (although the instance may be different).

*

The content reader code must ultimately arrange for a call to @@ -296,7 +303,7 @@ public class Content * race condition (the thread that reads with the thread that invokes the * demand callback).

* - * @return a chunk of content, possibly an error instance, or {@code null} + * @return a chunk of content, possibly a failure instance, or {@code null} * @see #demand(Runnable) * @see Retainable */ @@ -327,18 +334,38 @@ public class Content void demand(Runnable demandCallback); /** - *

Fails this content source, possibly failing and discarding accumulated - * content chunks that were not yet read.

+ *

Fails this content source with a {@link Chunk#isLast() last} {@link Chunk#getFailure() failure chunk}, + * failing and discarding accumulated content chunks that were not yet read.

*

The failure may be notified to the content reader at a later time, when - * the content reader reads a content chunk, via an {@link Chunk.Error} instance.

+ * the content reader reads a content chunk, via a {@link Chunk} instance + * with a non null {@link Chunk#getFailure()}.

*

If {@link #read()} has returned a last chunk, this is a no operation.

*

Typical failure: the content being aborted by user code, or idle timeouts.

*

If this method has already been called, then it is a no operation.

* * @param failure the cause of the failure + * @see Chunk#getFailure() */ void fail(Throwable failure); + /** + *

Fails this content source with a {@link Chunk#getFailure() failure chunk} + * that may or not may be {@link Chunk#isLast() last}. + * If {@code last} is {@code true}, then the failure is persistent and a call to this method acts + * as {@link #fail(Throwable)}. Otherwise the failure is transient and a + * {@link Chunk#getFailure() failure chunk} will be {@link #read() read} in order with content chunks, + * and subsequent calls to {@link #read() read} may produce other content.

+ *

A {@code Content.Source} or its {@link #read() reader} may treat a transient failure as persistent.

+ * + * @param failure A failure. + * @param last true if the failure is persistent, false if the failure is transient. + * @see Chunk#getFailure() + */ + default void fail(Throwable failure, boolean last) + { + fail(failure); + } + /** *

Rewinds this content, if possible, so that subsequent reads return * chunks starting from the beginning of this content.

@@ -555,14 +582,52 @@ public class Content } /** - *

Creates an {@link Error error chunk} with the given failure.

+ *

Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure + * and {@link Chunk#isLast()} returning true.

* * @param failure the cause of the failure - * @return a new Error.Chunk + * @return a new {@link Chunk#isFailure(Chunk) failure chunk} */ - static Error from(Throwable failure) + static Chunk from(Throwable failure) { - return new Error(failure); + return from(failure, true); + } + + /** + *

Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure + * and given {@link Chunk#isLast() last} state.

+ * + * @param failure the cause of the failure + * @param last true if the failure is terminal, else false for transient failure + * @return a new {@link Chunk#isFailure(Chunk) failure chunk} + */ + static Chunk from(Throwable failure, boolean last) + { + return new Chunk() + { + public Throwable getFailure() + { + return failure; + } + + @Override + public ByteBuffer getByteBuffer() + { + return BufferUtil.EMPTY_BUFFER; + } + + @Override + public boolean isLast() + { + return last; + } + + @Override + public String toString() + { + return String.format("Chunk@%x{c=%s,l=%b}", hashCode(), failure, last); + } + }; } /** @@ -581,8 +646,12 @@ public class Content * {@code null} * * + * {@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() last} * {@link Error Error} - * {@link Error Error} + * + * + * {@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() not last} + * {@code null} * * * {@link #isLast()} @@ -597,18 +666,57 @@ public class Content */ static Chunk next(Chunk chunk) { - if (chunk == null || chunk instanceof Error) - return chunk; + if (chunk == null) + return null; + if (Content.Chunk.isFailure(chunk)) + return chunk.isLast() ? chunk : null; if (chunk.isLast()) return EOF; return null; } + /** + * @param chunk The chunk to test for an {@link Chunk#getFailure() failure}. + * @return True if the chunk is non-null and {@link Chunk#getFailure() chunk.getError()} returns non-null. + */ + static boolean isFailure(Chunk chunk) + { + return chunk != null && chunk.getFailure() != null; + } + + /** + * @param chunk The chunk to test for an {@link Chunk#getFailure() failure} + * @param last The {@link Chunk#isLast() last} status to test for. + * @return True if the chunk is non-null and {@link Chunk#getFailure()} returns non-null + * and {@link Chunk#isLast()} matches the passed status. + */ + static boolean isFailure(Chunk chunk, boolean last) + { + return chunk != null && chunk.getFailure() != null && chunk.isLast() == last; + } + /** * @return the ByteBuffer of this Chunk */ ByteBuffer getByteBuffer(); + /** + * Get a failure (which may be from a {@link Source#fail(Throwable) failure} or + * a {@link Source#fail(Throwable, boolean) warning}), if any, associated with the chunk. + *
    + *
  • A {@code chunk} must not have a failure and a {@link #getByteBuffer()} with content.
  • + *
  • A {@code chunk} with a failure may or may not be {@link #isLast() last}.
  • + *
  • A {@code chunk} with a failure must not be {@link #canRetain() retainable}.
  • + *
+ * @return A {@link Throwable} indicating the failure or null if there is no failure or warning. + * @see Source#fail(Throwable) + * @see Source#fail(Throwable, boolean) + */ + default Throwable getFailure() + { + return null; + } + /** * @return whether this is the last Chunk */ @@ -674,46 +782,6 @@ public class Content return asChunk(getByteBuffer().asReadOnlyBuffer(), isLast(), this); } - /** - *

A chunk that wraps a failure.

- *

Error Chunks are always last and have no bytes to read, - * as such they are terminal Chunks.

- * - * @see #from(Throwable) - */ - final class Error implements Chunk - { - private final Throwable cause; - - private Error(Throwable cause) - { - this.cause = cause; - } - - public Throwable getCause() - { - return cause; - } - - @Override - public ByteBuffer getByteBuffer() - { - return BufferUtil.EMPTY_BUFFER; - } - - @Override - public boolean isLast() - { - return true; - } - - @Override - public String toString() - { - return String.format("%s@%x{c=%s}", getClass().getSimpleName(), hashCode(), cause); - } - } - /** *

Implementations of this interface may process {@link Chunk}s being copied by the * {@link Content#copy(Source, Sink, Processor, Callback)} method, so that diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/AsyncContent.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/AsyncContent.java index 6b695d17d2d..6bd5eeebc32 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/AsyncContent.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/AsyncContent.java @@ -51,8 +51,8 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable private final AutoLock.WithCondition lock = new AutoLock.WithCondition(); private final SerializedInvoker invoker = new SerializedInvoker(); - private final Queue chunks = new ArrayDeque<>(); - private Content.Chunk.Error errorChunk; + private final Queue chunks = new ArrayDeque<>(); + private Content.Chunk persistentFailure; private boolean readClosed; private boolean writeClosed; private Runnable demandCallback; @@ -62,7 +62,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable * {@inheritDoc} *

The write completes:

*
    - *
  • immediately with a failure when this instance is closed or already in error
  • + *
  • immediately with a failure when this instance is closed or already has a failure
  • *
  • successfully when a non empty {@link Content.Chunk} returned by {@link #read()} is released
  • *
  • successfully just before the {@link Content.Chunk} is returned by {@link #read()}, * for any empty chunk {@link Content.Chunk}.
  • @@ -79,7 +79,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable * or succeeded if and only if the chunk is terminal, as non-terminal * chunks have to bind the succeeding of the callback to their release. */ - private void offer(AsyncChunk chunk) + private void offer(Content.Chunk chunk) { Throwable failure = null; boolean wasEmpty = false; @@ -89,9 +89,9 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable { failure = new IOException("closed"); } - else if (errorChunk != null) + else if (persistentFailure != null) { - failure = errorChunk.getCause(); + failure = persistentFailure.getFailure(); } else { @@ -105,14 +105,14 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable if (length == UNDETERMINED_LENGTH) { length = 0; - for (AsyncChunk c : chunks) + for (Content.Chunk c : chunks) length += c.remaining(); } } } } - if (failure != null) - chunk.failed(failure); + if (failure != null && chunk instanceof AsyncChunk asyncChunk) + asyncChunk.failed(failure); if (wasEmpty) invoker.run(this::invokeDemandCallback); } @@ -125,14 +125,14 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable { // Always wrap the exception to make sure // the stack trace comes from flush(). - if (errorChunk != null) - throw new IOException(errorChunk.getCause()); + if (persistentFailure != null) + throw new IOException(persistentFailure.getFailure()); if (chunks.isEmpty()) return; // Special case for a last empty chunk that may not be read. if (writeClosed && chunks.size() == 1) { - AsyncChunk chunk = chunks.peek(); + Content.Chunk chunk = chunks.peek(); if (chunk.isLast() && !chunk.hasRemaining()) return; } @@ -171,7 +171,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable @Override public Content.Chunk read() { - AsyncChunk current; + Content.Chunk current; try (AutoLock.WithCondition condition = lock.lock()) { if (length == UNDETERMINED_LENGTH) @@ -181,8 +181,8 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable { if (readClosed) return Content.Chunk.EOF; - if (errorChunk != null) - return errorChunk; + if (persistentFailure != null) + return persistentFailure; return null; } readClosed = current.isLast(); @@ -195,7 +195,12 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable return current; // If the chunk is not reference counted, we can succeed it now and return a chunk with a noop release. - current.succeeded(); + if (current instanceof AsyncChunk asyncChunk) + asyncChunk.succeeded(); + + if (Content.Chunk.isFailure(current)) + return current; + return current.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY; } @@ -208,7 +213,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable if (this.demandCallback != null) throw new IllegalStateException("demand pending"); this.demandCallback = Objects.requireNonNull(demandCallback); - invoke = !chunks.isEmpty() || readClosed || errorChunk != null; + invoke = !chunks.isEmpty() || readClosed || persistentFailure != null; } if (invoke) invoker.run(this::invokeDemandCallback); @@ -241,22 +246,35 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable @Override public void fail(Throwable failure) { - List drained; + List drained; try (AutoLock.WithCondition condition = lock.lock()) { if (readClosed) return; - if (errorChunk != null) + if (persistentFailure != null) return; - errorChunk = Content.Chunk.from(failure); + persistentFailure = Content.Chunk.from(failure); drained = List.copyOf(chunks); chunks.clear(); condition.signal(); } - drained.forEach(ac -> ac.failed(failure)); + drained.forEach(c -> + { + if (c instanceof AsyncChunk ac) + ac.failed(failure); + }); invoker.run(this::invokeDemandCallback); } + @Override + public void fail(Throwable failure, boolean last) + { + if (last) + fail(failure); + else + offer(Content.Chunk.from(failure, false)); + } + public int count() { try (AutoLock ignored = lock.lock()) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceInputStream.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceInputStream.java index 40eeb47f817..c35a29828ee 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceInputStream.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceInputStream.java @@ -54,8 +54,12 @@ public class ContentSourceInputStream extends InputStream { if (chunk != null) { - if (chunk instanceof Content.Chunk.Error error) - throw IO.rethrow(error.getCause()); + if (Content.Chunk.isFailure(chunk)) + { + Content.Chunk c = chunk; + chunk = null; + throw IO.rethrow(c.getFailure()); + } ByteBuffer byteBuffer = chunk.getByteBuffer(); if (chunk.isLast() && !byteBuffer.hasRemaining()) @@ -96,8 +100,8 @@ public class ContentSourceInputStream extends InputStream @Override public void close() { - // If we have already reached a real EOF or an error, close is a noop. - if (chunk == Content.Chunk.EOF || chunk instanceof Content.Chunk.Error) + // If we have already reached a real EOF or a persistent failure, close is a noop. + if (chunk == Content.Chunk.EOF || Content.Chunk.isFailure(chunk, true)) return; // If we have a chunk here, then it needs to be released diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java index a5a95af1014..15858a928ff 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java @@ -125,10 +125,10 @@ public class ContentSourcePublisher implements Flow.Publisher return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { terminate(); - subscriber.onError(error.getCause()); + subscriber.onError(chunk.getFailure()); return; } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java index 2a14854e3a6..a299250c147 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java @@ -60,10 +60,10 @@ public abstract class ContentSourceTransformer implements Content.Source return null; } - if (rawChunk instanceof Content.Chunk.Error) + if (Content.Chunk.isFailure(rawChunk)) return rawChunk; - if (transformedChunk instanceof Content.Chunk.Error) + if (Content.Chunk.isFailure(transformedChunk)) return transformedChunk; transformedChunk = process(rawChunk); @@ -142,13 +142,14 @@ public abstract class ContentSourceTransformer implements Content.Source *

    The input chunk is released as soon as this method returns, so * implementations that must hold onto the input chunk must arrange to call * {@link Content.Chunk#retain()} and its correspondent {@link Content.Chunk#release()}.

    - *

    Implementations should return an {@link Content.Chunk.Error error chunk} in case + *

    Implementations should return an {@link Content.Chunk} with non-null + * {@link Content.Chunk#getFailure()} in case * of transformation errors.

    *

    Exceptions thrown by this method are equivalent to returning an error chunk.

    *

    Implementations of this method may return:

    *
      *
    • {@code null}, if more input chunks are necessary to produce an output chunk
    • - *
    • the {@code inputChunk} itself, typically in case of {@link Content.Chunk.Error}s, + *
    • the {@code inputChunk} itself, typically in case of non-null {@link Content.Chunk#getFailure()}, * or when no transformation is required
    • *
    • a new {@link Content.Chunk} derived from {@code inputChunk}.
    • *
    diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/InputStreamContentSource.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/InputStreamContentSource.java index 85010bdd8c8..acff23e4abe 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/InputStreamContentSource.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/InputStreamContentSource.java @@ -40,7 +40,7 @@ public class InputStreamContentSource implements Content.Source private final ByteBufferPool bufferPool; private int bufferSize = 4096; private Runnable demandCallback; - private Content.Chunk.Error errorChunk; + private Content.Chunk errorChunk; private boolean closed; public InputStreamContentSource(InputStream inputStream) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java index 3fc6dd7ac74..55eb68c85c9 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/PathContentSource.java @@ -46,7 +46,7 @@ public class PathContentSource implements Content.Source private SeekableByteChannel channel; private long totalRead; private Runnable demandCallback; - private Content.Chunk.Error errorChunk; + private Content.Chunk errorChunk; public PathContentSource(Path path) { diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java index 94c710dd0c0..ed294dea85f 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentCopier.java @@ -16,9 +16,13 @@ package org.eclipse.jetty.io.internal; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingNestedCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ContentCopier extends IteratingNestedCallback { + private static final Logger LOG = LoggerFactory.getLogger(ContentCopier.class); + private final Content.Source source; private final Content.Sink sink; private final Content.Chunk.Processor chunkProcessor; @@ -56,8 +60,15 @@ public class ContentCopier extends IteratingNestedCallback if (chunkProcessor != null && chunkProcessor.process(current, this)) return Action.SCHEDULED; - if (current instanceof Error error) - throw error.getCause(); + if (Content.Chunk.isFailure(current)) + { + if (current.isLast()) + throw current.getFailure(); + if (LOG.isDebugEnabled()) + LOG.debug("ignored transient failure", current.getFailure()); + succeeded(); + return Action.SCHEDULED; + } sink.write(current.isLast(), current.getByteBuffer(), this); return Action.SCHEDULED; diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceByteBuffer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceByteBuffer.java index 6d882b41cb8..9d286e42596 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceByteBuffer.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceByteBuffer.java @@ -44,9 +44,9 @@ public class ContentSourceByteBuffer implements Runnable return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - promise.failed(error.getCause()); + promise.failed(chunk.getFailure()); return; } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceConsumer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceConsumer.java index e59ab4ebef7..5992ee06e21 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceConsumer.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceConsumer.java @@ -41,9 +41,9 @@ public class ContentSourceConsumer implements Invocable.Task return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - callback.failed(error.getCause()); + callback.failed(chunk.getFailure()); return; } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceString.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceString.java index 9e0aaec9c81..68096a00571 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceString.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/internal/ContentSourceString.java @@ -42,9 +42,9 @@ public class ContentSourceString content.demand(this::convert); return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - promise.failed(error.getCause()); + promise.failed(chunk.getFailure()); return; } text.append(chunk.getByteBuffer()); diff --git a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/AsyncContentTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/AsyncContentTest.java index 02c6e1be3f9..4c68779fa9c 100644 --- a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/AsyncContentTest.java +++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/AsyncContentTest.java @@ -33,7 +33,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -101,7 +100,7 @@ public class AsyncContentTest // We must read the error. chunk = async.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); // Offering more should fail. CountDownLatch failLatch = new CountDownLatch(1); @@ -209,14 +208,14 @@ public class AsyncContentTest assertThat(chunk.release(), is(true)); callback1.assertNoFailureWithSuccesses(1); - Exception error1 = new Exception("test1"); - async.fail(error1); + Exception failure1 = new Exception("test1"); + async.fail(failure1); chunk = async.read(); - assertSame(error1, ((Content.Chunk.Error)chunk).getCause()); + assertSame(failure1, chunk.getFailure()); - callback2.assertSingleFailureSameInstanceNoSuccess(error1); - callback3.assertSingleFailureSameInstanceNoSuccess(error1); + callback2.assertSingleFailureSameInstanceNoSuccess(failure1); + callback3.assertSingleFailureSameInstanceNoSuccess(failure1); } } diff --git a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java index 9c055879b44..1f7fd012420 100644 --- a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java +++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTest.java @@ -15,8 +15,10 @@ package org.eclipse.jetty.io; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -27,6 +29,7 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -49,13 +52,14 @@ import org.junit.jupiter.params.provider.MethodSource; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class ContentSourceTest { @@ -237,7 +241,7 @@ public class ContentSourceTest // We must read the error. chunk = source.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); } @ParameterizedTest @@ -259,7 +263,7 @@ public class ContentSourceTest source.fail(new CancellationException()); Content.Chunk chunk = source.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); CountDownLatch latch = new CountDownLatch(1); source.demand(latch::countDown); @@ -285,7 +289,7 @@ public class ContentSourceTest }); chunk = source.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); } @Test @@ -560,4 +564,63 @@ public class ContentSourceTest { } } + + @Test + public void testAsyncContentWithWarnings() + { + AsyncContent content = new AsyncContent(); + + Content.Sink.write(content, false, "One", Callback.NOOP); + content.fail(new TimeoutException("test"), false); + Content.Sink.write(content, true, "Two", Callback.NOOP); + + Content.Chunk chunk = content.read(); + assertFalse(chunk.isLast()); + assertFalse(Content.Chunk.isFailure(chunk)); + assertThat(BufferUtil.toString(chunk.getByteBuffer()), is("One")); + + chunk = content.read(); + assertFalse(chunk.isLast()); + assertTrue(Content.Chunk.isFailure(chunk)); + assertThat(chunk.getFailure(), instanceOf(TimeoutException.class)); + + chunk = content.read(); + assertTrue(chunk.isLast()); + assertFalse(Content.Chunk.isFailure(chunk)); + assertThat(BufferUtil.toString(chunk.getByteBuffer()), is("Two")); + } + + @Test + public void testAsyncContentWithWarningsAsInputStream() throws Exception + { + AsyncContent content = new AsyncContent(); + + Content.Sink.write(content, false, "One", Callback.NOOP); + content.fail(new TimeoutException("test"), false); + Content.Sink.write(content, true, "Two", Callback.NOOP); + + InputStream in = Content.Source.asInputStream(content); + + byte[] buffer = new byte[1024]; + int len = in.read(buffer); + assertThat(len, is(3)); + assertThat(new String(buffer, 0, 3, StandardCharsets.ISO_8859_1), is("One")); + + try + { + int ignored = in.read(); + fail(); + } + catch (IOException ioe) + { + assertThat(ioe.getCause(), instanceOf(TimeoutException.class)); + } + + len = in.read(buffer); + assertThat(len, is(3)); + assertThat(new String(buffer, 0, 3, StandardCharsets.ISO_8859_1), is("Two")); + + len = in.read(buffer); + assertThat(len, is(-1)); + } } diff --git a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java index 0761f4b778d..820b09b9428 100644 --- a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java +++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java @@ -34,7 +34,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -259,11 +258,11 @@ public class ContentSourceTransformerTest chunk.release(); chunk = transformer.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); // Trying to read again returns the error again. chunk = transformer.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); // Make sure that the source is failed. assertEquals(0, source.count()); @@ -284,11 +283,11 @@ public class ContentSourceTransformerTest chunk.release(); chunk = transformer.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); // Trying to read again returns the error again. chunk = transformer.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); } @Test @@ -306,11 +305,11 @@ public class ContentSourceTransformerTest source.fail(new IOException()); chunk = transformer.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); // Trying to read again returns the error again. chunk = transformer.read(); - assertInstanceOf(Content.Chunk.Error.class, chunk); + assertTrue(Content.Chunk.isFailure(chunk, true)); } private static class WordSplitLowCaseTransformer extends ContentSourceTransformer 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 926ea4cea0b..1f7f949add1 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 @@ -160,9 +160,9 @@ public class FormFields extends CompletableFuture implements Runnable return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - completeExceptionally(error.getCause()); + completeExceptionally(chunk.getFailure()); return; } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpStream.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpStream.java index 96f5d29a9c5..f9f830f369d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpStream.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpStream.java @@ -26,7 +26,7 @@ import org.eclipse.jetty.util.StaticException; /** * A HttpStream is an abstraction that together with {@link MetaData.Request}, represents the * flow of data from and to a single request and response cycle. It is roughly analogous to the - * Stream within a HTTP/2 connection, in that a connection can have many streams, each used once + * Stream within an HTTP/2 connection, in that a connection can have many streams, each used once * and each representing a single request and response exchange. */ public interface HttpStream extends Callback @@ -42,7 +42,7 @@ public interface HttpStream extends Callback /** * @return an ID unique within the lifetime scope of the associated protocol connection. - * This may be a protocol ID (eg HTTP/2 stream ID) or it may be unrelated to the protocol. + * This may be a protocol ID (e.g. HTTP/2 stream ID) or it may be unrelated to the protocol. */ String getId(); @@ -50,7 +50,7 @@ public interface HttpStream extends Callback *

    Reads a chunk of content, with the same semantic as {@link Content.Source#read()}.

    *

    This method is called from the implementation of {@link Request#read()}.

    * - * @return a chunk of content, possibly an {@link Chunk.Error error} or {@code null}. + * @return a chunk of content, possibly with non-null {@link Chunk#getFailure()} or {@code null}. */ Content.Chunk read(); @@ -125,8 +125,8 @@ public interface HttpStream extends Callback content.release(); // if the input failed, then fail the stream for same reason - if (content instanceof Chunk.Error error) - return error.getCause(); + if (Content.Chunk.isFailure(content)) + return content.getFailure(); if (content.isLast()) return null; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index a44f82480c0..ae6a849c1b4 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -69,12 +69,14 @@ import org.eclipse.jetty.util.thread.Invocable; * return true; * } * - * if (chunk instanceof Content.Chunk.Error error) + * if (Content.Chunk.isError(chunk)) * { * Throwable failure = error.getCause(); * * // Handle errors. - * // Mark the handling as complete, either generating a custom + * // If the chunk is not last, then the error can be ignored and reading can be tried again. + * // Otherwise, if the chunk is last, or we do not wish to ignore a non-last error, then + * // mark the handling as complete, either generating a custom * // response and succeeding the callback, or failing the callback. * callback.failed(failure); * return true; 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 1efd4dfe389..90ecf7449b9 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 @@ -342,9 +342,9 @@ public class DelayedHandler extends Handler.Wrapper getRequest().demand(this::readAndParse); return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - _formData.completeExceptionally(error.getCause()); + _formData.completeExceptionally(chunk.getFailure()); return; } _formData.parse(chunk); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java index 4a3932be43f..c272e941438 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java @@ -229,7 +229,7 @@ public abstract class EventsHandler extends Handler.Wrapper * {@link Request#read()}). * * @param request the request object. The {@code read()}, {@code demand(Runnable)} and {@code fail(Throwable)} methods must not be called by the listener. - * @param chunk a potentially null request content chunk, including {@link org.eclipse.jetty.io.Content.Chunk.Error} + * @param chunk a potentially null request content chunk, including {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) error} * and {@link org.eclipse.jetty.http.Trailers} chunks. * If a reference to the chunk (or its {@link ByteBuffer}) is kept, * then {@link Content.Chunk#retain()} must be called. diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java index 64f84db3d13..6cb7d4f3821 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java @@ -268,7 +268,7 @@ public class StatisticsHandler extends EventsHandler protected class MinimumDataRateRequest extends Request.Wrapper { - private Content.Chunk.Error _errorContent; + private Content.Chunk _errorContent; private MinimumDataRateRequest(Request request) { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipRequest.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipRequest.java index b2891d6dc0e..1e3eb179861 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipRequest.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipRequest.java @@ -159,7 +159,7 @@ public class GzipRequest extends Request.Wrapper _chunk = inputChunk; if (_chunk == null) return null; - if (_chunk instanceof Content.Chunk.Error) + if (Content.Chunk.isFailure(_chunk)) return _chunk; if (_chunk.isLast() && !_chunk.hasRemaining()) return Content.Chunk.EOF; 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 f4315df47da..d5277662284 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 @@ -119,7 +119,7 @@ public class HttpChannelState implements HttpChannel, Components /** * Failure passed to {@link #onFailure(Throwable)} */ - private Content.Chunk.Error _failure; + private Content.Chunk _failure; /** * Listener for {@link #onFailure(Throwable)} events */ @@ -400,9 +400,9 @@ public class HttpChannelState implements HttpChannel, Components { _failure = Content.Chunk.from(x); } - else if (ExceptionUtil.areNotAssociated(_failure.getCause(), x) && _failure.getCause().getClass() != x.getClass()) + else if (ExceptionUtil.areNotAssociated(_failure.getFailure(), x) && _failure.getFailure().getClass() != x.getClass()) { - _failure.getCause().addSuppressed(x); + _failure.getFailure().addSuppressed(x); } // If not handled, then we just fail the request callback @@ -1243,8 +1243,8 @@ public class HttpChannelState implements HttpChannel, Components protected Throwable getFailure(HttpChannelState httpChannelState) { - Content.Chunk.Error failure = httpChannelState._failure; - return failure == null ? null : failure.getCause(); + Content.Chunk failure = httpChannelState._failure; + return failure == null ? null : failure.getFailure(); } /** @@ -1694,7 +1694,7 @@ public class HttpChannelState implements HttpChannel, Components protected void onError(Runnable task, Throwable failure) { ChannelRequest request; - Content.Chunk.Error error; + Content.Chunk error; boolean callbackCompleted; try (AutoLock ignore = _lock.lock()) { @@ -1726,9 +1726,9 @@ public class HttpChannelState implements HttpChannel, Components { // We are already in error, so we will not handle this one, // but we will add as suppressed if we have not seen it already. - Throwable cause = error.getCause(); + Throwable cause = error.getFailure(); if (ExceptionUtil.areNotAssociated(cause, failure)) - error.getCause().addSuppressed(failure); + error.getFailure().addSuppressed(failure); } } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 3587809fae4..3034d4d1f9f 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -1098,8 +1098,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ { BadMessageException bad = new BadMessageException("Early EOF"); - if (stream._chunk instanceof Error error) - error.getCause().addSuppressed(bad); + if (Content.Chunk.isFailure(stream._chunk)) + stream._chunk.getFailure().addSuppressed(bad); else { if (stream._chunk != null) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java index 0758b29508c..043a677525a 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java @@ -657,9 +657,9 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture request.demand(this); return; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - callback.failed(error.getCause()); + callback.failed(chunk.getFailure()); return; } // copy buffer diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulHandlerTest.java index 85cf73430aa..5b4a76d2d60 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulHandlerTest.java @@ -710,9 +710,9 @@ public class GracefulHandlerTest } } LOG.debug("chunk = {}", chunk); - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - Response.writeError(request, response, callback, error.getCause()); + Response.writeError(request, response, callback, chunk.getFailure()); return true; } bytesRead += chunk.remaining(); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelTest.java index 518428b9dcd..d9a48847169 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpChannelTest.java @@ -66,7 +66,6 @@ import static org.hamcrest.Matchers.sameInstance; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -1214,8 +1213,8 @@ public class HttpChannelTest Request rq = handling.get().getRequest(); Content.Chunk chunk = rq.read(); assertTrue(chunk.isLast()); - assertInstanceOf(Content.Chunk.Error.class, chunk); - assertThat(((Content.Chunk.Error)chunk).getCause(), sameInstance(failure)); + assertTrue(Content.Chunk.isFailure(chunk, true)); + assertThat(chunk.getFailure(), sameInstance(failure)); CountDownLatch demand = new CountDownLatch(1); // Callback serialized until after onError task diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java index 63f832d67ed..f95dfeae659 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java @@ -542,10 +542,10 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture continue; } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { earlyEOFException.countDown(); - throw IO.rethrow(error.getCause()); + throw IO.rethrow(chunk.getFailure()); } if (chunk.hasRemaining()) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java index de227e6a604..783b4e76304 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java @@ -105,9 +105,9 @@ public class DumpHandler extends Handler.Abstract } } - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - callback.failed(error.getCause()); + callback.failed(chunk.getFailure()); return true; } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java index 97f38c91c6c..17edad793c1 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java @@ -100,9 +100,9 @@ public class StatisticsHandlerTest return true; } - if (chunk instanceof Content.Chunk.Error errorContent) + if (Content.Chunk.isFailure(chunk)) { - callback.failed(errorContent.getCause()); + callback.failed(chunk.getFailure()); return true; } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java index 80146c46581..87e7dfbdaed 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java @@ -284,8 +284,8 @@ public class ThreadLimitHandlerTest request.demand(this); return; } - if (chunk instanceof Error error) - throw error.getCause(); + if (Content.Chunk.isFailure(chunk)) + throw chunk.getFailure(); if (chunk.hasRemaining()) read.addAndGet(chunk.remaining()); diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java index 734aee0224e..d31312d6b7d 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java @@ -63,7 +63,6 @@ import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -256,8 +255,8 @@ public class HttpClientTest extends AbstractTest continue; } } - if (chunk instanceof Content.Chunk.Error error) - throw IO.rethrow(error.getCause()); + if (Content.Chunk.isFailure(chunk)) + throw IO.rethrow(chunk.getFailure()); total += chunk.remaining(); if (total >= sleep) @@ -941,7 +940,7 @@ public class HttpClientTest extends AbstractTest assertThat(chunks2.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(totalBytes)); assertThat(chunks3.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(0)); assertThat(chunks3.size(), is(1)); - assertThat(chunks3.get(0), instanceOf(Content.Chunk.Error.class)); + assertTrue(Content.Chunk.isFailure(chunks3.get(0), true)); chunks1.forEach(Content.Chunk::release); chunks2.forEach(Content.Chunk::release); @@ -983,7 +982,7 @@ public class HttpClientTest extends AbstractTest assertThat(chunks3Latch.await(5, TimeUnit.SECONDS), is(true)); assertThat(chunks3.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(0)); assertThat(chunks3.size(), is(1)); - assertThat(chunks3.get(0), instanceOf(Content.Chunk.Error.class)); + assertTrue(Content.Chunk.isFailure(chunks3.get(0), true)); chunks1.forEach(Content.Chunk::release); chunks2.forEach(Content.Chunk::release); diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerTimeoutsTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerTimeoutsTest.java index 7cd669cb9fb..2e15b39e29b 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerTimeoutsTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerTimeoutsTest.java @@ -130,8 +130,8 @@ public class ServerTimeoutsTest extends AbstractTest // Reads should yield the idle timeout. Content.Chunk chunk = requestRef.get().read(); - assertThat(chunk, instanceOf(Content.Chunk.Error.class)); - Throwable cause = ((Content.Chunk.Error)chunk).getCause(); + assertTrue(Content.Chunk.isFailure(chunk, true)); + Throwable cause = chunk.getFailure(); assertThat(cause, instanceOf(TimeoutException.class)); // Complete the callback as the error listener promised. diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java index f06327be7fc..b422a681dc9 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java @@ -257,9 +257,9 @@ public class ProxyServlet extends AbstractProxyServlet public Content.Chunk read() { Content.Chunk chunk = super.read(); - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - onClientRequestFailure(request, proxyRequest, response, error.getCause()); + onClientRequestFailure(request, proxyRequest, response, chunk.getFailure()); } else { 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 b222c8ddd71..be69bb48113 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 @@ -33,7 +33,7 @@ import org.slf4j.LoggerFactory; class AsyncContentProducer implements ContentProducer { private static final Logger LOG = LoggerFactory.getLogger(AsyncContentProducer.class); - private static final Content.Chunk.Error RECYCLED_ERROR_CHUNK = Content.Chunk.from(new StaticException("ContentProducer has been recycled")); + private static final Content.Chunk RECYCLED_ERROR_CHUNK = Content.Chunk.from(new StaticException("ContentProducer has been recycled"), true); final AutoLock _lock; private final ServletChannel _servletChannel; @@ -101,10 +101,10 @@ class AsyncContentProducer implements ContentProducer public boolean isError() { assertLocked(); - boolean error = _chunk instanceof Content.Chunk.Error; + boolean failure = Content.Chunk.isFailure(_chunk); if (LOG.isDebugEnabled()) - LOG.debug("isError = {} {}", error, this); - return error; + LOG.debug("isFailure = {} {}", failure, this); + return failure; } @Override 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 4d095125700..bd5de581aec 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 @@ -255,14 +255,14 @@ public class HttpInput extends ServletInputStream implements Runnable return read; } - if (chunk instanceof Content.Chunk.Error errorChunk) + if (Content.Chunk.isFailure(chunk)) { - Throwable error = errorChunk.getCause(); + Throwable failure = chunk.getFailure(); if (LOG.isDebugEnabled()) - LOG.debug("read error={} {}", error, this); - if (error instanceof IOException) - throw (IOException)error; - throw new IOException(error); + LOG.debug("read failure={} {}", failure, this); + if (failure instanceof IOException) + throw (IOException)failure; + throw new IOException(failure); } if (LOG.isDebugEnabled()) @@ -343,14 +343,14 @@ public class HttpInput extends ServletInputStream implements Runnable return; } - if (chunk instanceof Content.Chunk.Error errorChunk) + if (Content.Chunk.isFailure(chunk)) { - Throwable error = errorChunk.getCause(); + Throwable failure = chunk.getFailure(); if (LOG.isDebugEnabled()) - LOG.debug("running error={} {}", error, this); + LOG.debug("running failure={} {}", failure, this); // TODO is this necessary to add here? _servletChannel.getServletContextResponse().getHeaders().add(HttpFields.CONNECTION_CLOSE); - _readListener.onError(error); + _readListener.onError(failure); } else if (chunk.isLast() && !chunk.hasRemaining()) { diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpInput.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpInput.java index 7d26536972e..9f729ca8809 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpInput.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpInput.java @@ -572,8 +572,8 @@ public class HttpInput extends ServletInputStream implements Runnable { public static Content asChunk(org.eclipse.jetty.io.Content.Chunk chunk) { - if (chunk instanceof org.eclipse.jetty.io.Content.Chunk.Error error) - return new ErrorContent(error.getCause()); + if (org.eclipse.jetty.io.Content.Chunk.isFailure(chunk)) + return new ErrorContent(chunk.getFailure()); if (chunk.isLast() && !chunk.hasRemaining()) return new EofContent(); Content content = new Content(chunk.getByteBuffer()) diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java index 0c455d5f6d8..a58bf1ef72e 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java @@ -257,9 +257,9 @@ public class ProxyServlet extends AbstractProxyServlet public Content.Chunk read() { Content.Chunk chunk = super.read(); - if (chunk instanceof Content.Chunk.Error error) + if (Content.Chunk.isFailure(chunk)) { - onClientRequestFailure(request, proxyRequest, response, error.getCause()); + onClientRequestFailure(request, proxyRequest, response, chunk.getFailure()); } else { diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/ClientDisconnectTest.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/ClientDisconnectTest.java index 18515b92fa0..97909fb5c8a 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/ClientDisconnectTest.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/ClientDisconnectTest.java @@ -26,7 +26,6 @@ import org.eclipse.jetty.ee9.websocket.api.annotations.WebSocket; import org.eclipse.jetty.ee9.websocket.client.WebSocketClient; import org.eclipse.jetty.ee9.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.io.ArrayByteBufferPool; -import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.BufferUtil; From 4e19fac07a8d833e66ece900d28a6bd550524d6b Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Fri, 23 Jun 2023 03:17:58 -0400 Subject: [PATCH 29/63] Bulletproof AbstractProxyServlet#destory() to make it easier to write (#9938) unit tests for custom subclasses of AbstractProxyServlet Co-authored-by: Greg Wilkins --- .../jetty/proxy/AbstractProxyServlet.java | 7 +++- .../jetty/proxy/AbstractProxyServletTest.java | 39 +++++++++++++++++++ .../proxy/AsyncMiddleManServletTest.java | 13 +++++-- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index 34a2529e799..eb8901706d7 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -49,6 +49,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.HttpCookieStore; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.slf4j.Logger; @@ -145,11 +146,13 @@ public abstract class AbstractProxyServlet extends HttpServlet { try { - _client.stop(); + LifeCycle.stop(_client); } catch (Exception x) { - if (_log.isDebugEnabled()) + if (_log == null) + x.printStackTrace(); + else if (_log.isDebugEnabled()) _log.debug("Failed to stop client", x); } } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java new file mode 100644 index 00000000000..f9c5f5af220 --- /dev/null +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java @@ -0,0 +1,39 @@ +// +// ======================================================================== +// 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.proxy; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.api.Response.CompleteListener; +import org.junit.jupiter.api.Test; + +public class AbstractProxyServletTest +{ + + @Test + public void testNewDestroy() throws Exception + { + new AbstractProxyServlet() + { + private static final long serialVersionUID = 1L; + + @Override + protected CompleteListener newProxyResponseListener(HttpServletRequest clientRequest, HttpServletResponse proxyResponse) + { + return null; + } + }.destroy(); + } +} diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java index 365ece15cdd..ffe2bf51ecc 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java @@ -73,6 +73,7 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.Utf8StringBuilder; import org.eclipse.jetty.util.ajax.JSON; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -159,9 +160,15 @@ public class AsyncMiddleManServletTest @AfterEach public void dispose() throws Exception { - client.stop(); - proxy.stop(); - server.stop(); + LifeCycle.stop(client); + LifeCycle.stop(proxy); + LifeCycle.stop(proxy); + } + + @Test + public void testNewDestroy() throws Exception + { + new AsyncMiddleManServlet().destroy(); } @Test From 32399b14d26a1cd242dcff0d5eb7028958d569f0 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 23 Jun 2023 10:05:24 +0200 Subject: [PATCH 30/63] Fixed bad merge --- .../org/eclipse/jetty/proxy/AbstractProxyServletTest.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java index f9c5f5af220..b5b8d543e1f 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractProxyServletTest.java @@ -13,17 +13,15 @@ package org.eclipse.jetty.proxy; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.Response.CompleteListener; import org.junit.jupiter.api.Test; public class AbstractProxyServletTest { - @Test - public void testNewDestroy() throws Exception + public void testNewDestroy() { new AbstractProxyServlet() { From 8f4a15c7da27b7a389933fe214a9dcaf66843694 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Fri, 23 Jun 2023 14:44:12 +0200 Subject: [PATCH 31/63] #9946 Stop passing Handler in constructor as a parent (#9948) #9946 Stop passing Handler in constructor as a parent Signed-off-by: Ludovic Orban --- .../server/session/SessionDocs.java | 2 +- .../server/websocket/WebSocketServerDocs.java | 17 +- .../org/eclipse/jetty/server/Handler.java | 15 -- .../jetty/server/handler/ContextHandler.java | 8 - .../server/handler/MovedContextHandler.java | 8 - .../org/eclipse/jetty/server/StopTest.java | 188 +++++++++--------- .../jetty/ee10/jsp/TestJettyJspServlet.java | 2 +- .../eclipse/jetty/ee10/demos/Http2Server.java | 2 +- .../jetty/ee10/demos/ManyServletContexts.java | 7 +- .../eclipse/jetty/ee10/demos/ProxyServer.java | 3 +- .../eclipse/jetty/ee10/ChatServletTest.java | 3 +- .../jetty/ee10/DispatchServletTest.java | 3 +- .../ee10/fcgi/proxy/TryFilesFilterTest.java | 3 +- .../ee10/proxy/AsyncMiddleManServletTest.java | 6 +- .../jetty/ee10/proxy/BalancerServletTest.java | 3 +- .../jetty/ee10/proxy/ClientAuthProxyTest.java | 2 +- .../ee10/proxy/ForwardProxyServerTest.java | 3 +- .../eclipse/jetty/ee10/proxy/ProxyServer.java | 3 +- .../ee10/proxy/ProxyServletFailureTest.java | 6 +- .../ee10/proxy/ProxyServletLoadTest.java | 6 +- .../jetty/ee10/proxy/ProxyServletTest.java | 6 +- .../jetty/ee10/proxy/ReverseProxyTest.java | 6 +- .../org/eclipse/jetty/ee10/runner/Runner.java | 6 +- .../ee10/servlet/ServletContextHandler.java | 28 +-- .../jetty/ee10/servlet/ServletTester.java | 3 +- .../servlet/AsyncContextListenersTest.java | 3 +- .../servlet/AsyncServletLongPollTest.java | 3 +- .../ee10/servlet/DefaultServletTest.java | 2 +- .../ee10/servlet/DispatcherForwardTest.java | 3 +- .../eclipse/jetty/ee10/servlet/FormTest.java | 3 +- .../servlet/GzipHandlerBreakEvenSizeTest.java | 2 +- .../jetty/ee10/servlet/GzipHandlerTest.java | 2 +- .../jetty/ee10/servlet/InitServletTest.java | 2 +- .../ee10/servlet/MultiPartServletTest.java | 2 +- .../jetty/ee10/servlet/RegexServletTest.java | 2 +- .../ee10/servlet/SSLAsyncIOServletTest.java | 6 +- .../servlet/ServletContextHandlerTest.java | 46 +++-- .../ee10/servlet/ServletLifeCycleTest.java | 3 +- .../ee10/servlets/AbstractDoSFilterTest.java | 3 +- .../ee10/servlets/CrossOriginFilterTest.java | 3 +- .../jetty/ee10/servlets/DoSFilterJMXTest.java | 3 +- .../ee10/servlets/EventSourceServletTest.java | 3 +- .../jetty/ee10/servlets/HeaderFilterTest.java | 3 +- .../IncludeExcludeBasedFilterTest.java | 3 +- .../jetty/ee10/servlets/QoSFilterTest.java | 3 +- .../ee10/servlets/ThreadStarvationTest.java | 2 +- .../ee10/test/HttpInputIntegrationTest.java | 3 +- .../ee10/session/SessionTestSupport.java | 3 +- .../session/RequestDispatchedSessionTest.java | 3 +- .../jetty/ee10/webapp/WebAppContext.java | 37 +--- .../jetty/ee10/webapp/WebAppContextTest.java | 2 +- .../ee10/webapp/WebAppDefaultServletTest.java | 2 +- .../websocket/jakarta/tests/LocalServer.java | 3 +- .../jakarta/tests/server/MemoryUsageTest.java | 3 +- .../tests/server/ServerDecoderTest.java | 2 +- .../tests/WebSocketOverHTTP2Test.java | 3 +- .../jetty/ee9/nested/ContextHandler.java | 2 +- .../jetty/ee9/nested/HandlerWrapper.java | 15 ++ .../jetty/ee9/webapp/WebAppContext.java | 3 +- 59 files changed, 263 insertions(+), 259 deletions(-) diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java index 69dbaf253da..0e4b730757c 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java @@ -78,7 +78,7 @@ public class SessionDocs //tag:schsession[] Server server = new Server(); - ServletContextHandler context = new ServletContextHandler(server, "/foo", ServletContextHandler.SESSIONS); + ServletContextHandler context = new ServletContextHandler("/foo", ServletContextHandler.SESSIONS); SessionHandler sessions = context.getSessionHandler(); //make idle sessions valid for only 5mins sessions.setMaxInactiveInterval(300); diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java index 575060f850b..8a3ad718107 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java @@ -63,7 +63,7 @@ public class WebSocketServerDocs Server server = new Server(8080); // Create a ServletContextHandler with the given context path. - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); server.setHandler(handler); // Ensure that JavaxWebSocketServletContainerInitializer is initialized, @@ -82,7 +82,7 @@ public class WebSocketServerDocs Server server = new Server(8080); // Create a ServletContextHandler with the given context path. - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); server.setHandler(handler); // Ensure that JavaxWebSocketServletContainerInitializer is initialized, @@ -137,7 +137,7 @@ public class WebSocketServerDocs Server server = new Server(8080); // Create a ServletContextHandler with the given context path. - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); server.setHandler(handler); // Setup the ServerContainer and the WebSocket endpoints for this web application context. @@ -169,7 +169,7 @@ public class WebSocketServerDocs Server server = new Server(8080); // Create a ServletContextHandler with the given context path. - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); server.setHandler(handler); // Ensure that JettyWebSocketServletContainerInitializer is initialized, @@ -188,7 +188,7 @@ public class WebSocketServerDocs Server server = new Server(8080); // Create a ServletContextHandler with the given context path. - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); server.setHandler(handler); // Ensure that JettyWebSocketServletContainerInitializer is initialized, @@ -234,7 +234,7 @@ public class WebSocketServerDocs Server server = new Server(8080); // Create a ServletContextHandler with the given context path. - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); server.setHandler(handler); // Setup the JettyWebSocketServerContainer and the WebSocket endpoints for this web application context. @@ -301,7 +301,7 @@ public class WebSocketServerDocs Server server = new Server(8080); // Create a ServletContextHandler with the given context path. - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); server.setHandler(handler); // Setup the JettyWebSocketServerContainer to initialize WebSocket components. @@ -357,7 +357,8 @@ public class WebSocketServerDocs Server server = new Server(8080); // tag::uriTemplatePathSpec[] - ServletContextHandler handler = new ServletContextHandler(server, "/ctx"); + ServletContextHandler handler = new ServletContextHandler("/ctx"); + server.setHandler(handler); // Configure the JettyWebSocketServerContainer. JettyWebSocketServletContainerInitializer.configure(handler, (servletContext, container) -> diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java index 0b528f65404..280d2a25820 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java @@ -216,21 +216,6 @@ public interface Handler extends LifeCycle, Destroyable, Request.Handler } return null; } - - /** - *

    Make a {@link Container} the parent of a {@link Handler}

    - * @param parent The {@link Container} that will be the parent - * @param handler The {@link Handler} that will be the child - */ - static void setAsParent(Container parent, Handler handler) - { - if (parent instanceof Collection collection) - collection.addHandler(handler); - else if (parent instanceof Singleton wrapper) - wrapper.setHandler(handler); - else if (parent != null) - throw new IllegalArgumentException("Unknown parent type: " + parent); - } } /** diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 26c9ef7ab81..1d47f5e6232 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -180,14 +180,6 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias _classLoader = classLoader; } - @Deprecated - public ContextHandler(Handler.Container parent, String contextPath) - { - this(contextPath); - Container.setAsParent(parent, this); - - } - @Override public void setServer(Server server) { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java index 5d35b42ed22..931f713136c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java @@ -18,7 +18,6 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.PreEncodedHttpField; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; @@ -42,13 +41,6 @@ public class MovedContextHandler extends ContextHandler setAllowNullPathInContext(true); } - public MovedContextHandler(Handler.Container parent, String contextPath, String redirectURI) - { - Handler.Container.setAsParent(parent, this); - setContextPath(contextPath); - setRedirectURI(redirectURI); - } - /** * @return the redirect status code, by default 303 */ diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java index 02634dc9488..8406d6f1a72 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/StopTest.java @@ -44,6 +44,7 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; @@ -92,7 +93,7 @@ public class StopTest } catch (Exception e) { - e.printStackTrace(); + throw new RuntimeException(e); } }); stopper.start(); @@ -107,10 +108,7 @@ public class StopTest ).getBytes()); client.getOutputStream().flush(); - while (!connector.isShutdown()) - { - Thread.sleep(10); - } + await().atMost(10, TimeUnit.SECONDS).until(connector::isShutdown); handler.latchB.countDown(); @@ -280,11 +278,9 @@ public class StopTest LocalConnector connector = new LocalConnector(server); server.addConnector(connector); - StatisticsHandler stats = new StatisticsHandler(); - server.setHandler(stats); - ContextHandler context = new ContextHandler("/"); - stats.setHandler(context); + StatisticsHandler stats = new StatisticsHandler(context); + server.setHandler(stats); Exchanger exchanger0 = new Exchanger<>(); Exchanger exchanger1 = new Exchanger<>(); @@ -314,53 +310,51 @@ public class StopTest server.setStopTimeout(1000); server.start(); - LocalEndPoint endp = connector.executeRequest( - "GET / HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "\r\n" - ); - - exchanger0.exchange(null); - exchanger1.exchange(null); - - String response = endp.getResponse(); - assertThat(response, containsString("200 OK")); - assertThat(response, Matchers.not(containsString("Connection: close"))); - - endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n")); - - exchanger0.exchange(null); - - FutureCallback stopped = new FutureCallback(); - new Thread(() -> + try (LocalEndPoint endp = connector.executeRequest( + """ + GET / HTTP/1.1\r + Host: localhost\r + \r + """ + )) { - try - { - server.stop(); - stopped.succeeded(); - } - catch (Throwable e) - { - stopped.failed(e); - } - }).start(); + exchanger0.exchange(null); + exchanger1.exchange(null); - long start = NanoTime.now(); - while (!connector.isShutdown()) - { - assertThat(NanoTime.secondsSince(start), lessThan(10L)); - Thread.sleep(10); + String response = endp.getResponse(); + assertThat(response, containsString("200 OK")); + assertThat(response, Matchers.not(containsString("Connection: close"))); + + endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n")); + + exchanger0.exchange(null); + + FutureCallback stopped = new FutureCallback(); + new Thread(() -> + { + try + { + server.stop(); + stopped.succeeded(); + } + catch (Throwable e) + { + stopped.failed(e); + } + }).start(); + + await().atMost(10, TimeUnit.SECONDS).until(connector::isShutdown); + + // Check new connections rejected! + assertThrows(IllegalStateException.class, () -> connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n")); + + // Check completed 200 has close + exchanger1.exchange(null); + response = endp.getResponse(); + assertThat(response, containsString("200 OK")); + assertThat(response, Matchers.containsString("Connection: close")); + stopped.get(10, TimeUnit.SECONDS); } - - // Check new connections rejected! - assertThrows(IllegalStateException.class, () -> connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n")); - - // Check completed 200 has close - exchanger1.exchange(null); - response = endp.getResponse(); - assertThat(response, containsString("200 OK")); - assertThat(response, Matchers.containsString("Connection: close")); - stopped.get(10, TimeUnit.SECONDS); } @Test @@ -373,7 +367,6 @@ public class StopTest ContextHandler context = new ContextHandler("/"); server.setHandler(context); - StatisticsHandler stats = new StatisticsHandler(); context.setHandler(stats); @@ -402,50 +395,51 @@ public class StopTest server.start(); - LocalEndPoint endp = connector.executeRequest( - "GET / HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "\r\n" - ); - - exchanger0.exchange(null); - exchanger1.exchange(null); - - String response = endp.getResponse(); - assertThat(response, containsString("200 OK")); - assertThat(response, Matchers.not(containsString("Connection: close"))); - - endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n")); - exchanger0.exchange(null); - - CountDownLatch latch = new CountDownLatch(1); - new Thread(() -> + try (LocalEndPoint endp = connector.executeRequest( + """ + GET / HTTP/1.1\r + Host: localhost\r + \r + """ + )) { - try + exchanger0.exchange(null); + exchanger1.exchange(null); + + String response = endp.getResponse(); + assertThat(response, containsString("200 OK")); + assertThat(response, Matchers.not(containsString("Connection: close"))); + + endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n")); + exchanger0.exchange(null); + + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { - context.stop(); - latch.countDown(); - } - catch (Exception e) - { - e.printStackTrace(); - } - }).start(); - while (context.isStarted()) - { - Thread.sleep(10); + try + { + context.stop(); + latch.countDown(); + } + catch (Exception e) + { + e.printStackTrace(); + } + }).start(); + + await().atMost(10, TimeUnit.SECONDS).until(context::isStopped); + + // Check new connections accepted, but don't find context! + String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"); + assertThat(unavailable, containsString(" 404 Not Found")); + + // Check completed 200 does not have close + exchanger1.exchange(null); + response = endp.getResponse(); + assertThat(response, containsString("200 OK")); + assertThat(response, Matchers.not(Matchers.containsString("Connection: close"))); + assertTrue(latch.await(10, TimeUnit.SECONDS)); } - - // Check new connections accepted, but don't find context! - String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"); - assertThat(unavailable, containsString(" 404 Not Found")); - - // Check completed 200 does not have close - exchanger1.exchange(null); - response = endp.getResponse(); - assertThat(response, containsString("200 OK")); - assertThat(response, Matchers.not(Matchers.containsString("Connection: close"))); - assertTrue(latch.await(10, TimeUnit.SECONDS)); } @Test @@ -479,12 +473,12 @@ public class StopTest ContextHandler context2 = new ContextHandler("/two") { @Override - protected void doStart() throws Exception + protected void doStart() { context2Started.set(true); } }; - contexts.setHandlers(new Handler[]{context0, context1, context2}); + contexts.setHandlers(context0, context1, context2); try { diff --git a/jetty-ee10/jetty-ee10-apache-jsp/src/test/java/org/eclipse/jetty/ee10/jsp/TestJettyJspServlet.java b/jetty-ee10/jetty-ee10-apache-jsp/src/test/java/org/eclipse/jetty/ee10/jsp/TestJettyJspServlet.java index 4d324c8ca58..95660e24a89 100644 --- a/jetty-ee10/jetty-ee10-apache-jsp/src/test/java/org/eclipse/jetty/ee10/jsp/TestJettyJspServlet.java +++ b/jetty-ee10/jetty-ee10-apache-jsp/src/test/java/org/eclipse/jetty/ee10/jsp/TestJettyJspServlet.java @@ -69,7 +69,7 @@ public class TestJettyJspServlet _server = new Server(); _connector = new LocalConnector(_server); _server.addConnector(_connector); - ServletContextHandler context = new ServletContextHandler(_server, "/context", true, false); + ServletContextHandler context = new ServletContextHandler("/context", true, false); _server.setHandler(context); context.setClassLoader(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); ServletHolder jspHolder = context.addServlet(JettyJspServlet.class, "/*"); diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/Http2Server.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/Http2Server.java index 453a4c74d02..830aea62458 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/Http2Server.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/Http2Server.java @@ -70,7 +70,7 @@ public class Http2Server server.addBean(LoggerFactory.getILoggerFactory()); - ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS); + ServletContextHandler context = new ServletContextHandler("/", ServletContextHandler.SESSIONS); Path docroot = Paths.get("src/main/resources/docroot"); if (!Files.exists(docroot)) throw new FileNotFoundException(docroot.toString()); diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ManyServletContexts.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ManyServletContexts.java index 270f9a78a9b..cb294e69f8b 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ManyServletContexts.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ManyServletContexts.java @@ -38,16 +38,17 @@ public class ManyServletContexts server.setHandler(contexts); // Configure context "/" (root) for servlets - ServletContextHandler root = new ServletContextHandler(contexts, "/", + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + contexts.addHandler(root); // Add servlets to root context root.addServlet(new ServletHolder(new HelloServlet("Hello")), "/"); root.addServlet(new ServletHolder(new HelloServlet("Ciao")), "/it/*"); root.addServlet(new ServletHolder(new HelloServlet("Bonjour")), "/fr/*"); // Configure context "/other" for servlets - ServletContextHandler other = new ServletContextHandler(contexts, - "/other", ServletContextHandler.SESSIONS); + ServletContextHandler other = new ServletContextHandler("/other", ServletContextHandler.SESSIONS); + contexts.addHandler(other); // Add servlets to /other context other.addServlet(DefaultServlet.class.getCanonicalName(), "/"); other.addServlet(new ServletHolder(new HelloServlet("YO!")), "*.yo"); diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java index f8fabe3b98b..8a10e650dc7 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java @@ -36,8 +36,9 @@ public class ProxyServer server.setHandler(proxy); // Setup proxy servlet - ServletContextHandler context = new ServletContextHandler(proxy, "/", + ServletContextHandler context = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + proxy.setHandler(context); ServletHolder proxyServlet = new ServletHolder(ProxyServlet.class); proxyServlet.setInitParameter("blackList", "www.eclipse.org"); context.addServlet(proxyServlet, "/*"); diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/ChatServletTest.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/ChatServletTest.java index 343c1741191..2fdf01e15f9 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/ChatServletTest.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/ChatServletTest.java @@ -36,7 +36,8 @@ public class ChatServletTest server = new Server(); connector = new LocalConnector(server); server.addConnector(connector); - ServletContextHandler context = new ServletContextHandler(server, "/"); + ServletContextHandler context = new ServletContextHandler("/"); + server.setHandler(context); ServletHolder dispatch = context.addServlet(ChatServlet.class, "/chat/*"); dispatch.setInitParameter("asyncTimeout", "500"); server.start(); diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/DispatchServletTest.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/DispatchServletTest.java index 689b476597f..0e77fc6c88b 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/DispatchServletTest.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/test/java/org/eclipse/jetty/ee10/DispatchServletTest.java @@ -44,7 +44,8 @@ public class DispatchServletTest server = new Server(); connector = new LocalConnector(server); server.addConnector(connector); - context = new ServletContextHandler(server, "/tests"); + context = new ServletContextHandler("/tests"); + server.setHandler(context); server.start(); } diff --git a/jetty-ee10/jetty-ee10-fcgi-proxy/src/test/java/org/eclipse/jetty/ee10/fcgi/proxy/TryFilesFilterTest.java b/jetty-ee10/jetty-ee10-fcgi-proxy/src/test/java/org/eclipse/jetty/ee10/fcgi/proxy/TryFilesFilterTest.java index 5f01c7e3d62..eb21f34cdb4 100644 --- a/jetty-ee10/jetty-ee10-fcgi-proxy/src/test/java/org/eclipse/jetty/ee10/fcgi/proxy/TryFilesFilterTest.java +++ b/jetty-ee10/jetty-ee10-fcgi-proxy/src/test/java/org/eclipse/jetty/ee10/fcgi/proxy/TryFilesFilterTest.java @@ -55,7 +55,8 @@ public class TryFilesFilterTest sslConnector = new ServerConnector(server, serverSslContextFactory); server.addConnector(sslConnector); - ServletContextHandler context = new ServletContextHandler(server, "/"); + ServletContextHandler context = new ServletContextHandler("/"); + server.setHandler(context); FilterHolder filterHolder = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); forwardPath = "/index.php"; diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServletTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServletTest.java index 26e321fe73e..04aaa7f735d 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServletTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServletTest.java @@ -109,7 +109,8 @@ public class AsyncMiddleManServletTest serverConnector = new ServerConnector(server); server.addConnector(serverConnector); - ServletContextHandler appCtx = new ServletContextHandler(server, "/", true, false); + ServletContextHandler appCtx = new ServletContextHandler("/", true, false); + server.setHandler(appCtx); ServletHolder appServletHolder = new ServletHolder(servlet); appCtx.addServlet(appServletHolder, "/*"); @@ -136,7 +137,8 @@ public class AsyncMiddleManServletTest proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration)); proxy.addConnector(proxyConnector); - ServletContextHandler proxyContext = new ServletContextHandler(proxy, "/", true, false); + ServletContextHandler proxyContext = new ServletContextHandler("/", true, false); + proxy.setHandler(proxyContext); this.proxyServlet = proxyServlet; ServletHolder proxyServletHolder = new ServletHolder(proxyServlet); proxyServletHolder.setInitParameters(initParams); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/BalancerServletTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/BalancerServletTest.java index a84a46a09fb..d55f6e087be 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/BalancerServletTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/BalancerServletTest.java @@ -93,7 +93,8 @@ public class BalancerServletTest ServerConnector connector = new ServerConnector(server); server.addConnector(connector); - ServletContextHandler context = new ServletContextHandler(server, CONTEXT_PATH, ServletContextHandler.SESSIONS); + ServletContextHandler context = new ServletContextHandler(CONTEXT_PATH, ServletContextHandler.SESSIONS); + server.setHandler(context); context.addServlet(servletHolder, SERVLET_PATH + "/*"); if (nodeName != null) diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java index 01d711e6796..21e6b2c74d5 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java @@ -177,7 +177,7 @@ public class ClientAuthProxyTest proxyConnector = new ServerConnector(proxy, 1, 1, ssl, http); proxy.addConnector(proxyConnector); - ServletContextHandler context = new ServletContextHandler(proxy, "/"); + ServletContextHandler context = new ServletContextHandler("/"); context.addServlet(new ServletHolder(servlet), "/*"); proxy.setHandler(context); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java index de52033dc23..ce2600e3c1b 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java @@ -109,7 +109,8 @@ public class ForwardProxyServerTest connectHandler.setConnectTimeout(1000); proxy.setHandler(connectHandler); - ServletContextHandler proxyHandler = new ServletContextHandler(connectHandler, "/"); + ServletContextHandler proxyHandler = new ServletContextHandler("/"); + connectHandler.setHandler(proxyHandler); proxyHandler.addServlet(new ServletHolder(proxyServlet), "/*"); proxy.start(); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java index 0020839fa32..cf82c427fc0 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java @@ -35,7 +35,8 @@ public class ProxyServer server.setHandler(proxy); // Setup proxy servlet - ServletContextHandler context = new ServletContextHandler(proxy, "/", ServletContextHandler.SESSIONS); + ServletContextHandler context = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + proxy.setHandler(context); ServletHolder proxyServlet = new ServletHolder(ProxyServlet.class); // proxyServlet.setInitParameter("whiteList", "google.com, www.eclipse.org, localhost"); // proxyServlet.setInitParameter("blackList", "google.com/calendar/*, www.eclipse.org/committers/"); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletFailureTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletFailureTest.java index 53b4a758627..a13a2cec020 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletFailureTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletFailureTest.java @@ -99,7 +99,8 @@ public class ProxyServletFailureTest proxy.addConnector(proxyConnector); proxyConnector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false); - ServletContextHandler proxyCtx = new ServletContextHandler(proxy, "/", true, false); + ServletContextHandler proxyCtx = new ServletContextHandler("/", true, false); + proxy.setHandler(proxyCtx); ServletHolder proxyServletHolder = new ServletHolder(proxyServlet); proxyServletHolder.setInitParameters(initParams); @@ -129,7 +130,8 @@ public class ProxyServletFailureTest serverConnector = new ServerConnector(server); server.addConnector(serverConnector); - ServletContextHandler appCtx = new ServletContextHandler(server, "/", true, false); + ServletContextHandler appCtx = new ServletContextHandler("/", true, false); + server.setHandler(appCtx); ServletHolder appServletHolder = new ServletHolder(servlet); appCtx.addServlet(appServletHolder, "/*"); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletLoadTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletLoadTest.java index 8feaeabb8de..e1c38b39e94 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletLoadTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletLoadTest.java @@ -77,7 +77,8 @@ public class ProxyServletLoadTest serverConnector = new ServerConnector(server); server.addConnector(serverConnector); - ServletContextHandler appCtx = new ServletContextHandler(server, "/", true, false); + ServletContextHandler appCtx = new ServletContextHandler("/", true, false); + server.setHandler(appCtx); ServletHolder appServletHolder = new ServletHolder(servlet); appCtx.addServlet(appServletHolder, "/*"); @@ -96,7 +97,8 @@ public class ProxyServletLoadTest proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration)); proxy.addConnector(proxyConnector); - ServletContextHandler proxyContext = new ServletContextHandler(proxy, "/", true, false); + ServletContextHandler proxyContext = new ServletContextHandler("/", true, false); + proxy.setHandler(proxyContext); ServletHolder proxyServletHolder = new ServletHolder(proxyServlet); proxyContext.addServlet(proxyServletHolder, "/*"); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java index 748ab57a578..ccd90c8a275 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java @@ -152,7 +152,8 @@ public class ProxyServletTest new HttpConnectionFactory()); server.addConnector(tlsServerConnector); - ServletContextHandler appCtx = new ServletContextHandler(server, "/", true, false); + ServletContextHandler appCtx = new ServletContextHandler("/", true, false); + server.setHandler(appCtx); ServletHolder appServletHolder = new ServletHolder(servlet); appCtx.addServlet(appServletHolder, "/*"); @@ -184,7 +185,8 @@ public class ProxyServletTest proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration)); proxy.addConnector(proxyConnector); - proxyContext = new ServletContextHandler(proxy, "/", true, false); + proxyContext = new ServletContextHandler("/", true, false); + proxy.setHandler(proxyContext); this.proxyServlet = proxyServlet; ServletHolder proxyServletHolder = new ServletHolder(proxyServlet); proxyServletHolder.setInitParameters(initParams); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ReverseProxyTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ReverseProxyTest.java index 1c1bf0a4825..a3239975ea5 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ReverseProxyTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ReverseProxyTest.java @@ -49,7 +49,8 @@ public class ReverseProxyTest serverConnector = new ServerConnector(server); server.addConnector(serverConnector); - ServletContextHandler appCtx = new ServletContextHandler(server, "/", true, false); + ServletContextHandler appCtx = new ServletContextHandler("/", true, false); + server.setHandler(appCtx); ServletHolder appServletHolder = new ServletHolder(servlet); appCtx.addServlet(appServletHolder, "/*"); @@ -66,7 +67,8 @@ public class ReverseProxyTest proxyConnector = new ServerConnector(proxy, new HttpConnectionFactory(configuration)); proxy.addConnector(proxyConnector); - ServletContextHandler proxyContext = new ServletContextHandler(proxy, "/", true, false); + ServletContextHandler proxyContext = new ServletContextHandler("/", true, false); + proxy.setHandler(proxyContext); ServletHolder proxyServletHolder = new ServletHolder(new AsyncMiddleManServlet() { @Override diff --git a/jetty-ee10/jetty-ee10-runner/src/main/java/org/eclipse/jetty/ee10/runner/Runner.java b/jetty-ee10/jetty-ee10-runner/src/main/java/org/eclipse/jetty/ee10/runner/Runner.java index a982a988833..989a9f451d7 100644 --- a/jetty-ee10/jetty-ee10-runner/src/main/java/org/eclipse/jetty/ee10/runner/Runner.java +++ b/jetty-ee10/jetty-ee10-runner/src/main/java/org/eclipse/jetty/ee10/runner/Runner.java @@ -358,8 +358,9 @@ public class Runner statsHandler.setHandler(oldHandler); _server.setHandler(statsHandler); - ServletContextHandler statsContext = new ServletContextHandler(_contexts, "/stats"); + ServletContextHandler statsContext = new ServletContextHandler("/stats"); statsContext.setSessionHandler(new SessionHandler()); + _contexts.addHandler(statsContext); if (_statsPropFile != null) { ResourceFactory resourceFactory = ResourceFactory.of(statsContext); @@ -457,10 +458,11 @@ public class Runner else { // assume it is a WAR file - WebAppContext webapp = new WebAppContext(_contexts, ctx.toString(), contextPath); + WebAppContext webapp = new WebAppContext(ctx.toString(), contextPath); webapp.setConfigurationClasses(PLUS_CONFIGURATION_CLASSES); webapp.setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN, CONTAINER_INCLUDE_JAR_PATTERN); + _contexts.addHandler(webapp); } //reset diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java index 38c50548bfb..d7393b17b5f 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java @@ -240,46 +240,40 @@ public class ServletContextHandler extends ContextHandler public ServletContextHandler(String contextPath) { - this(null, contextPath); + this(contextPath, null, null, null, null); } public ServletContextHandler(int options) { - this(null, null, options); + this(null, options); } - public ServletContextHandler(Container parent, String contextPath) + public ServletContextHandler(String contextPath, int options) { - this(parent, contextPath, null, null, null, null); + this(contextPath, null, null, null, null, options); } - public ServletContextHandler(Container parent, String contextPath, int options) + public ServletContextHandler(String contextPath, boolean sessions, boolean security) { - this(parent, contextPath, null, null, null, null, options); + this(contextPath, (sessions ? SESSIONS : 0) | (security ? SECURITY : 0)); } - public ServletContextHandler(Container parent, String contextPath, boolean sessions, boolean security) + public ServletContextHandler(SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler) { - this(parent, contextPath, (sessions ? SESSIONS : 0) | (security ? SECURITY : 0)); + this(null, sessionHandler, securityHandler, servletHandler, errorHandler); } - public ServletContextHandler(Container parent, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler) + public ServletContextHandler(String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler) { - this(parent, null, sessionHandler, securityHandler, servletHandler, errorHandler); + this(contextPath, sessionHandler, securityHandler, servletHandler, errorHandler, 0); } - public ServletContextHandler(Container parent, String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler) - { - this(parent, contextPath, sessionHandler, securityHandler, servletHandler, errorHandler, 0); - } - - public ServletContextHandler(Container parent, String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler, int options) + public ServletContextHandler(String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler, int options) { _servletContext = newServletContextApi(); if (contextPath != null) setContextPath(contextPath); - Container.setAsParent(parent, this); _options = options; _sessionHandler = sessionHandler; diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java index a76e06a1e16..205e918d66e 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java @@ -217,7 +217,8 @@ public class ServletTester extends ContainerLifeCycle public ServletTester(String contextPath, int options) { - _context = new ServletContextHandler(_server, contextPath, options); + _context = new ServletContextHandler(contextPath, options); + _server.setHandler(_context); _server.setConnectors(new Connector[]{_connector}); addBean(_server); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncContextListenersTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncContextListenersTest.java index 2d5dc3b7ee3..29dc40276ab 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncContextListenersTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncContextListenersTest.java @@ -47,8 +47,9 @@ public class AsyncContextListenersTest _connector = new ServerConnector(_server); _server.addConnector(_connector); - ServletContextHandler context = new ServletContextHandler(_server, "/", false, false); + ServletContextHandler context = new ServletContextHandler("/", false, false); context.addServlet(new ServletHolder(servlet), path); + _server.setHandler(context); _server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletLongPollTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletLongPollTest.java index db18472d785..2bedf4c2312 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletLongPollTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletLongPollTest.java @@ -47,7 +47,8 @@ public class AsyncServletLongPollTest connector = new ServerConnector(server); server.addConnector(connector); String contextPath = "/context"; - context = new ServletContextHandler(server, contextPath, ServletContextHandler.NO_SESSIONS); + context = new ServletContextHandler(contextPath, ServletContextHandler.NO_SESSIONS); + server.setHandler(context); ServletHolder servletHolder = new ServletHolder(servlet); String servletPath = "/path"; context.addServlet(servletHolder, servletPath); diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java index 262d50ae2c7..213a4399634 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java @@ -3188,7 +3188,7 @@ public class DefaultServletTest @Test public void testPathInfoOnly() throws Exception { - ServletContextHandler context = new ServletContextHandler(null, "/c1", ServletContextHandler.NO_SESSIONS); + ServletContextHandler context = new ServletContextHandler("/c1", ServletContextHandler.NO_SESSIONS); context.setWelcomeFiles(new String[]{"index.y", "index.x"}); ServletHolder indexServlet = new ServletHolder("index-servlet", new HttpServlet() { diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherForwardTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherForwardTest.java index b7a4584db48..2f913a0a218 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherForwardTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DispatcherForwardTest.java @@ -50,9 +50,10 @@ public class DispatcherForwardTest connector = new LocalConnector(server); server.addConnector(connector); - ServletContextHandler context = new ServletContextHandler(server, "/"); + ServletContextHandler context = new ServletContextHandler("/"); context.addServlet(new ServletHolder(servlet1), "/one"); context.addServlet(new ServletHolder(servlet2), "/two"); + server.setHandler(context); server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/FormTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/FormTest.java index 10aa41361a1..8280ee66f63 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/FormTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/FormTest.java @@ -63,9 +63,10 @@ public class FormTest connector = new ServerConnector(server, 1, 1); server.addConnector(connector); - ServletContextHandler handler = new ServletContextHandler(server, contextPath); + ServletContextHandler handler = new ServletContextHandler(contextPath); HttpServlet servlet = config.apply(handler); handler.addServlet(new ServletHolder(servlet), servletPath + "/*"); + server.setHandler(handler); server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerBreakEvenSizeTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerBreakEvenSizeTest.java index 3bbea06e110..ecfddc29e31 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerBreakEvenSizeTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerBreakEvenSizeTest.java @@ -55,7 +55,7 @@ public class GzipHandlerBreakEvenSizeTest GzipHandler gzipHandler = new GzipHandler(); - ServletContextHandler context = new ServletContextHandler(gzipHandler, "/"); + ServletContextHandler context = new ServletContextHandler("/"); context.addServlet(VeryCompressibleContentServlet.class, "/content"); gzipHandler.setHandler(context); server.setHandler(gzipHandler); diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java index f78a37bd8b1..5106802c9fe 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/GzipHandlerTest.java @@ -125,7 +125,7 @@ public class GzipHandlerTest _server.setHandler(gzipHandler); gzipHandler.setHandler(context); } - + _server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/InitServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/InitServletTest.java index dc19a813ef1..0376b1a9a47 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/InitServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/InitServletTest.java @@ -90,7 +90,7 @@ public class InitServletTest public void testServletInitialization() throws Exception { Server server = new Server(0); - ServletContextHandler context = new ServletContextHandler(server, "/"); + ServletContextHandler context = new ServletContextHandler("/"); server.setHandler(context); // Add a lazily instantiated servlet. context.addServlet(new ServletHolder(DemoServlet.class), "/*"); 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 868430c179b..2ccbbde45b9 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 @@ -109,7 +109,7 @@ public class MultiPartServletTest connector = new ServerConnector(server); server.addConnector(connector); - ServletContextHandler contextHandler = new ServletContextHandler(server, "/"); + ServletContextHandler contextHandler = new ServletContextHandler("/"); ServletHolder servletHolder = new ServletHolder(servlet); servletHolder.getRegistration().setMultipartConfig(config); contextHandler.addServlet(servletHolder, "/"); diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/RegexServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/RegexServletTest.java index 816888a22c5..dbc6bf72d04 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/RegexServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/RegexServletTest.java @@ -41,7 +41,7 @@ public class RegexServletTest _server = new Server(); _connector = new LocalConnector(_server); - _servletContextHandler = new ServletContextHandler(_server, "/ctx"); + _servletContextHandler = new ServletContextHandler("/ctx"); _servletContextHandler.setServletHandler(new ServletHandler() { @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SSLAsyncIOServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SSLAsyncIOServletTest.java index b5d557c2bca..da23dd6ebd0 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SSLAsyncIOServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SSLAsyncIOServletTest.java @@ -177,7 +177,8 @@ public class SSLAsyncIOServletTest server.addConnector(connector); contextPath = "/context"; - ServletContextHandler context = new ServletContextHandler(server, contextPath, true, false); + ServletContextHandler context = new ServletContextHandler(contextPath, true, false); + server.setHandler(context); servletPath = "/servlet"; context.addServlet(new ServletHolder(servlet), servletPath); @@ -225,7 +226,8 @@ public class SSLAsyncIOServletTest server.addConnector(connector); contextPath = "/context"; - ServletContextHandler context = new ServletContextHandler(server, contextPath, true, false); + ServletContextHandler context = new ServletContextHandler(contextPath, true, false); + server.setHandler(context); servletPath = "/servlet"; context.addServlet(new ServletHolder(servlet), servletPath); diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java index eda08b82516..e6b4a9a9eaf 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletContextHandlerTest.java @@ -689,7 +689,7 @@ public class ServletContextHandlerTest public void testInitParams() throws Exception { //Test get/setInitParam with null throws NPE - ServletContextHandler root = new ServletContextHandler(_server, "/", ServletContextHandler.SESSIONS); + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); _server.setHandler(root); ListenerHolder initialListener = new ListenerHolder(); initialListener.setListener(new ServletContextListener() @@ -782,7 +782,8 @@ public class ServletContextHandlerTest int startMin = 7; Integer timeout = Integer.valueOf(100); - ServletContextHandler root = new ServletContextHandler(contexts, "/", ServletContextHandler.SESSIONS); + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + contexts.addHandler(root); root.getSessionHandler().setMaxInactiveInterval((int)TimeUnit.MINUTES.toSeconds(startMin)); root.addBean(new MySCIStarter(root.getContext(), new MySCI(true, timeout.intValue())), true); _server.start(); @@ -806,7 +807,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/", ServletContextHandler.SESSIONS); + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + contexts.addHandler(root); ListenerHolder listenerHolder = new ListenerHolder(); StopTestListener stopTestListener = new StopTestListener(); listenerHolder.setListener(stopTestListener); @@ -837,7 +839,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/", ServletContextHandler.SESSIONS); + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + contexts.addHandler(root); MySessionHandler sessions = new MySessionHandler(); root.setSessionHandler(sessions); @@ -855,7 +858,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); root.addBean(new MySCIStarter(root.getContext(), new MySCI()), true); _server.start(); assertTrue((Boolean)root.getServletContext().getAttribute("MySCI.startup")); @@ -876,7 +880,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); class TestServletContextListener implements ServletContextListener { public int initialized = 0; @@ -910,7 +915,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/", ServletContextHandler.SESSIONS); + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + contexts.addHandler(root); root.getSessionHandler().setSessionDomain("testing"); ListenerHolder initialListener = new ListenerHolder(); initialListener.setListener(new InitialListener()); @@ -1022,7 +1028,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/", ServletContextHandler.SESSIONS); + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + contexts.addHandler(root); SessionHandler session = root.getSessionHandler(); ServletHandler servlet = root.getServletHandler(); @@ -1107,7 +1114,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); class FilterCreatingSCI implements ServletContainerInitializer { @Override @@ -1153,7 +1161,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); class ListenerCreatingSCI implements ServletContainerInitializer { @Override @@ -1345,7 +1354,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); class ServletAddingSCI implements ServletContainerInitializer { @Override @@ -1378,7 +1388,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); ServletHolder jspServlet = new ServletHolder(); jspServlet.setName("jsp"); jspServlet.setHeldClass(FakeJspServlet.class); @@ -1416,7 +1427,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); ServletHolder jspServlet = new ServletHolder(); jspServlet.setName("jsp"); jspServlet.setHeldClass(FakeJspServlet.class); @@ -1453,7 +1465,8 @@ public class ServletContextHandlerTest ContextHandlerCollection contexts = new ContextHandlerCollection(); _server.setHandler(contexts); - ServletContextHandler root = new ServletContextHandler(contexts, "/"); + ServletContextHandler root = new ServletContextHandler("/"); + contexts.addHandler(root); ServletHolder jspServlet = new ServletHolder(); jspServlet.setName("jsp"); jspServlet.setHeldClass(FakeJspServlet.class); @@ -1897,7 +1910,8 @@ public class ServletContextHandlerTest Handler.Sequence list = new Handler.Sequence(); _server.setHandler(list); - ServletContextHandler root = new ServletContextHandler(list, "/", ServletContextHandler.SESSIONS); + ServletContextHandler root = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + list.addHandler(root); ServletHandler servlet = root.getServletHandler(); servlet.setEnsureDefaultServlet(false); @@ -2435,7 +2449,7 @@ public class ServletContextHandlerTest @Test public void testEmptyPathInfo() throws Exception { - ServletContextHandler context = new ServletContextHandler(null, "/c1", ServletContextHandler.NO_SESSIONS); + ServletContextHandler context = new ServletContextHandler("/c1", ServletContextHandler.NO_SESSIONS); context.setAllowNullPathInContext(true); context.addServlet(new ServletHolder("default-servlet", new HttpServlet() { diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletLifeCycleTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletLifeCycleTest.java index b116177af43..7486038b4ee 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletLifeCycleTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletLifeCycleTest.java @@ -51,7 +51,8 @@ public class ServletLifeCycleTest LocalConnector connector = new LocalConnector(server); server.addConnector(connector); - ServletContextHandler context = new ServletContextHandler(server, "/"); + ServletContextHandler context = new ServletContextHandler("/"); + server.setHandler(context); context.getObjectFactory().addDecorator(new TestDecorator()); diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/AbstractDoSFilterTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/AbstractDoSFilterTest.java index de448ad2eae..af9984776c4 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/AbstractDoSFilterTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/AbstractDoSFilterTest.java @@ -60,7 +60,8 @@ public abstract class AbstractDoSFilterTest _server = new Server(); _connector = new ServerConnector(_server); _server.addConnector(_connector); - ServletContextHandler context = new ServletContextHandler(_server, "/ctx", true, false); + ServletContextHandler context = new ServletContextHandler("/ctx", true, false); + _server.setHandler(context); DefaultSessionCache sessionCache = new DefaultSessionCache(context.getSessionHandler()); FileSessionDataStore fileStore = new FileSessionDataStore(); diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilterTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilterTest.java index 44396026ef3..01e2e86c3a4 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilterTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/CrossOriginFilterTest.java @@ -58,7 +58,8 @@ public class CrossOriginFilterTest server = new Server(); connector = new LocalConnector(server); server.addConnector(connector); - context = new ServletContextHandler(server, "/"); + context = new ServletContextHandler("/"); + server.setHandler(context); server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/DoSFilterJMXTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/DoSFilterJMXTest.java index d17f16b0aa7..2873fe04f47 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/DoSFilterJMXTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/DoSFilterJMXTest.java @@ -48,7 +48,8 @@ public class DoSFilterJMXTest Connector connector = new ServerConnector(server); server.addConnector(connector); - ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS); + ServletContextHandler context = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + server.setHandler(context); DoSFilter filter = new DoSFilter(); FilterHolder holder = new FilterHolder(filter); String name = "dos"; diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/EventSourceServletTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/EventSourceServletTest.java index 68eb92a385e..858ba92c03b 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/EventSourceServletTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/EventSourceServletTest.java @@ -51,7 +51,8 @@ public class EventSourceServletTest connector = (NetworkConnector)server.getConnectors()[0]; String contextPath = "/test"; - context = new ServletContextHandler(server, contextPath, ServletContextHandler.SESSIONS); + context = new ServletContextHandler(contextPath, ServletContextHandler.SESSIONS); + server.setHandler(context); server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/HeaderFilterTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/HeaderFilterTest.java index 618539ad90a..dba283d3dc5 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/HeaderFilterTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/HeaderFilterTest.java @@ -50,8 +50,9 @@ public class HeaderFilterTest _server = new Server(); _connector = new LocalConnector(_server); _server.addConnector(_connector); - _context = new ServletContextHandler(_server, "/context"); + _context = new ServletContextHandler("/context"); _context.addServlet(NullServlet.class, "/test/*"); + _server.setHandler(_context); _server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/IncludeExcludeBasedFilterTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/IncludeExcludeBasedFilterTest.java index b4516b1721b..ba6e299ee54 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/IncludeExcludeBasedFilterTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/IncludeExcludeBasedFilterTest.java @@ -51,7 +51,8 @@ public class IncludeExcludeBasedFilterTest _server = new Server(); _connector = new LocalConnector(_server); _server.addConnector(_connector); - _context = new ServletContextHandler(_server, "/context"); + _context = new ServletContextHandler("/context"); + _server.setHandler(_context); _context.addServlet(NullServlet.class, "/test/*"); _server.start(); } diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/QoSFilterTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/QoSFilterTest.java index 9286c3cf47e..d8edf7e7210 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/QoSFilterTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/QoSFilterTest.java @@ -64,7 +64,8 @@ public class QoSFilterTest public void setUp() throws Exception { server = new Server(); - context = new ServletContextHandler(server, "/context"); + context = new ServletContextHandler("/context"); + server.setHandler(context); context.addServlet(TestServlet.class, "/test"); TestServlet.__maxSleepers = 0; TestServlet.__sleepers = 0; diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java index 837f57b0ff7..c627c9b0147 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java @@ -104,7 +104,7 @@ public class ThreadStarvationTest connector.setIdleTimeout(Long.MAX_VALUE); _server.addConnector(connector); - ServletContextHandler context = new ServletContextHandler(_server, "/"); + ServletContextHandler context = new ServletContextHandler("/"); context.setBaseResourceAsPath(directory.toPath()); //TODO: Uses DefaultServlet, currently all commented out diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java index bccf3d3a2e6..e816fc9fdd1 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java @@ -118,7 +118,8 @@ public class HttpInputIntegrationTest http2.setIdleTimeout(5000); __server.addConnector(http2); - ServletContextHandler context = new ServletContextHandler(__server, "/ctx"); + ServletContextHandler context = new ServletContextHandler("/ctx"); + __server.setHandler(context); ServletHolder holder = new ServletHolder(new TestServlet()); holder.setAsyncSupported(true); context.addServlet(holder, "/*"); diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/main/java/org/eclipse/jetty/ee10/session/SessionTestSupport.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/main/java/org/eclipse/jetty/ee10/session/SessionTestSupport.java index df6a10f6f51..a76feba7105 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/main/java/org/eclipse/jetty/ee10/session/SessionTestSupport.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/main/java/org/eclipse/jetty/ee10/session/SessionTestSupport.java @@ -120,7 +120,8 @@ public class SessionTestSupport public ServletContextHandler addContext(String contextPath) throws Exception { - ServletContextHandler context = new ServletContextHandler(_contexts, contextPath); + ServletContextHandler context = new ServletContextHandler(contextPath); + _contexts.addHandler(context); SessionHandler sessionHandler = newSessionHandler(); sessionHandler.setSessionIdManager(_sessionIdManager); sessionHandler.setMaxInactiveInterval(_maxInactivePeriod); diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/RequestDispatchedSessionTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/RequestDispatchedSessionTest.java index 6a27952010a..0fb478167e9 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/RequestDispatchedSessionTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/RequestDispatchedSessionTest.java @@ -56,7 +56,8 @@ public class RequestDispatchedSessionTest ContextHandlerCollection handlers = new ContextHandlerCollection(); // Default session behavior - ServletContextHandler contextHandler = new ServletContextHandler(handlers, "/", ServletContextHandler.SESSIONS); + ServletContextHandler contextHandler = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + handlers.addHandler(contextHandler); contextHandler.addServlet(LoginServlet.class, "/login"); contextHandler.addServlet(ShowUserServlet.class, "/user"); contextHandler.addServlet(DefaultServlet.class, "/"); diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java index b216575f4a0..e782cab0471 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java @@ -49,7 +49,6 @@ import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Deployable; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.ExceptionUtil; import org.eclipse.jetty.util.IO; @@ -150,7 +149,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL public WebAppContext() { - this(null, null, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); + this(null, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); } /** @@ -159,7 +158,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL */ public WebAppContext(String webApp, String contextPath) { - this(null, contextPath, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); + this(contextPath, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); setWar(webApp); } @@ -169,29 +168,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL */ public WebAppContext(Resource webApp, String contextPath) { - this(null, contextPath, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); - setWarResource(webApp); - } - - /** - * @param parent The parent container. - * @param contextPath The context path - * @param webApp The URL or filename of the webapp directory or war file. - */ - public WebAppContext(Container parent, String webApp, String contextPath) - { - this(parent, contextPath, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); - setWar(webApp); - } - - /** - * @param parent The parent container. - * @param contextPath The context path - * @param webApp The webapp directory or war file. - */ - public WebAppContext(Container parent, Resource webApp, String contextPath) - { - this(parent, contextPath, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); + this(contextPath, null, null, null, new ErrorPageErrorHandler(), SESSIONS | SECURITY); setWarResource(webApp); } @@ -203,11 +180,10 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL */ public WebAppContext(SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler) { - this(null, null, sessionHandler, securityHandler, servletHandler, errorHandler, 0); + this(null, sessionHandler, securityHandler, servletHandler, errorHandler, 0); } /** - * @param parent the parent container * @param contextPath the context path * @param sessionHandler SessionHandler for this web app * @param securityHandler SecurityHandler for this web app @@ -215,14 +191,13 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL * @param errorHandler ErrorHandler for this web app * @param options the options ({@link ServletContextHandler#SESSIONS} and/or {@link ServletContextHandler#SECURITY}) */ - public WebAppContext(Container parent, String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler, int options) + public WebAppContext(String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler, int options) { // always pass parent as null and then set below, so that any resulting setServer call // is done after this instance is constructed. - super(null, contextPath, sessionHandler, securityHandler, servletHandler, errorHandler, options); + super(contextPath, sessionHandler, securityHandler, servletHandler, errorHandler, options); setErrorHandler(errorHandler != null ? errorHandler : new ErrorPageErrorHandler()); setProtectedTargets(__dftProtectedTargets); - Handler.Container.setAsParent(parent, this); } @Override diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java index b6123479743..1fd5c44b5cd 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java @@ -440,7 +440,7 @@ public class WebAppContextTest Server server = newServer(); ContextHandlerCollection contexts = new ContextHandlerCollection(); - WebAppContext context = new WebAppContext(null, null, null, null, null, new ErrorPageErrorHandler(), + WebAppContext context = new WebAppContext(null, null, null, null, new ErrorPageErrorHandler(), ServletContextHandler.NO_SESSIONS | ServletContextHandler.NO_SECURITY); context.setContextPath("/"); diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppDefaultServletTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppDefaultServletTest.java index 6b8aad537b7..c447730566f 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppDefaultServletTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppDefaultServletTest.java @@ -86,7 +86,7 @@ public class WebAppDefaultServletTest output.write("standard hash dir welcome".getBytes(StandardCharsets.UTF_8)); } - WebAppContext context = new WebAppContext(server, directoryPath.toString(), "/"); + WebAppContext context = new WebAppContext(directoryPath.toString(), "/"); server.setHandler(context); server.start(); diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/LocalServer.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/LocalServer.java index f0e4029e45b..566c81e3401 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/LocalServer.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/LocalServer.java @@ -161,7 +161,8 @@ public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provi protected Handler createRootHandler(Server server) throws Exception { - servletContextHandler = new ServletContextHandler(server, "/", true, false); + servletContextHandler = new ServletContextHandler("/", true, false); + server.setHandler(servletContextHandler); servletContextHandler.setContextPath("/"); JakartaWebSocketServletContainerInitializer.configure(servletContextHandler, (context, container) -> ((JakartaWebSocketServerContainer)container).addSessionListener(trackingListener)); diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/MemoryUsageTest.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/MemoryUsageTest.java index 234fbab02cf..ddabdf72b54 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/MemoryUsageTest.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/MemoryUsageTest.java @@ -72,7 +72,8 @@ public class MemoryUsageTest connector = new ServerConnector(server); server.addConnector(connector); - ServletContextHandler contextHandler = new ServletContextHandler(server, "/", true, false); + ServletContextHandler contextHandler = new ServletContextHandler("/", true, false); + server.setHandler(contextHandler); JakartaWebSocketServletContainerInitializer.configure(contextHandler, (context, container) -> { ServerEndpointConfig config = ServerEndpointConfig.Builder.create(BasicEndpoint.class, "/").build(); diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/ServerDecoderTest.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/ServerDecoderTest.java index 21653f05eb2..7c63393b2ed 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/ServerDecoderTest.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/server/ServerDecoderTest.java @@ -103,7 +103,7 @@ public class ServerDecoderTest server = new Server(); ServerConnector serverConnector = new ServerConnector(server); server.addConnector(serverConnector); - ServletContextHandler servletContextHandler = new ServletContextHandler(null, "/"); + ServletContextHandler servletContextHandler = new ServletContextHandler("/"); server.setHandler(servletContextHandler); JakartaWebSocketServletContainerInitializer.configure(servletContextHandler, ((servletContext, serverContainer) -> diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java index abb5430ed31..ffab4054f09 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java @@ -121,7 +121,8 @@ public class WebSocketOverHTTP2Test tlsConnector = new ServerConnector(server, 1, 1, ssl, alpn, h1s, h2s); server.addConnector(tlsConnector); - context = new ServletContextHandler(server, "/"); + context = new ServletContextHandler("/"); + server.setHandler(context); context.addServlet(new ServletHolder(servlet), "/ws/*"); JettyWebSocketServletContainerInitializer.configure(context, null); 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 6d855dd40b6..6dde17d0f2e 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 @@ -248,7 +248,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie _initParams = new HashMap<>(); if (contextPath != null) setContextPath(contextPath); - Handler.Container.setAsParent(parent, _coreContextHandler); + HandlerWrapper.setAsParent(parent, _coreContextHandler); } @Override diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HandlerWrapper.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HandlerWrapper.java index c6e7c1900c1..17759204f3d 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HandlerWrapper.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HandlerWrapper.java @@ -138,4 +138,19 @@ public class HandlerWrapper extends AbstractHandlerContainer } super.destroy(); } + + /** + *

    Make a {@link org.eclipse.jetty.server.Handler.Container} the parent of a {@link org.eclipse.jetty.server.Handler}

    + * @param parent The {@link org.eclipse.jetty.server.Handler.Container} that will be the parent + * @param handler The {@link org.eclipse.jetty.server.Handler} that will be the child + */ + public static void setAsParent(org.eclipse.jetty.server.Handler.Container parent, org.eclipse.jetty.server.Handler handler) + { + if (parent instanceof org.eclipse.jetty.server.Handler.Collection collection) + collection.addHandler(handler); + else if (parent instanceof org.eclipse.jetty.server.Handler.Singleton wrapper) + wrapper.setHandler(handler); + else if (parent != null) + throw new IllegalArgumentException("Unknown parent type: " + parent); + } } diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java index 6aeec5ef551..17bfdcbd7ee 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java @@ -42,6 +42,7 @@ import jakarta.servlet.http.HttpSessionIdListener; import jakarta.servlet.http.HttpSessionListener; import org.eclipse.jetty.ee9.nested.ContextHandler; import org.eclipse.jetty.ee9.nested.ErrorHandler; +import org.eclipse.jetty.ee9.nested.HandlerWrapper; import org.eclipse.jetty.ee9.nested.SessionHandler; import org.eclipse.jetty.ee9.security.ConstraintAware; import org.eclipse.jetty.ee9.security.ConstraintMapping; @@ -234,7 +235,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL _apiContext = new Context(); setErrorHandler(errorHandler != null ? errorHandler : new ErrorPageErrorHandler()); setProtectedTargets(__dftProtectedTargets); - Handler.Container.setAsParent(parent, this.get()); + HandlerWrapper.setAsParent(parent, this.get()); } @Override From 151d487fb9c07fb8f3b22b881ec448fcebebf7df Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 23 Jun 2023 16:06:58 +0200 Subject: [PATCH 32/63] Update plexus-utils to 4.0.0 --- jetty-maven-plugin/pom.xml | 4 ++++ pom.xml | 8 +++++++- tests/test-websocket-autobahn/pom.xml | 13 ++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/jetty-maven-plugin/pom.xml b/jetty-maven-plugin/pom.xml index f28ec134c85..6b0bac97ce3 100644 --- a/jetty-maven-plugin/pom.xml +++ b/jetty-maven-plugin/pom.xml @@ -160,6 +160,10 @@ org.codehaus.plexus plexus-utils + + org.codehaus.plexus + plexus-xml + org.apache.maven.plugin-tools maven-plugin-tools-api diff --git a/pom.xml b/pom.xml index 00afeede81f..e14ea7ee15d 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,8 @@ 1.2.0 1.2.0 2.1.1 - 3.5.1 + 4.0.0 + 4.0.0 2.0.5 2.1.1.RELEASE 1.2.5 @@ -1256,6 +1257,11 @@ plexus-utils ${plexus-utils.version} + + org.codehaus.plexus + plexus-xml + ${plexus-xml.version} + org.eclipse.jetty apache-jsp diff --git a/tests/test-websocket-autobahn/pom.xml b/tests/test-websocket-autobahn/pom.xml index 7505b770425..66f8ccfa565 100644 --- a/tests/test-websocket-autobahn/pom.xml +++ b/tests/test-websocket-autobahn/pom.xml @@ -60,6 +60,17 @@ plexus-utils test + + org.codehaus.plexus + plexus-xml + test + + + javax.annotation + javax.annotation-api + + + com.googlecode.json-simple json-simple @@ -131,4 +142,4 @@ - \ No newline at end of file + From f6e963c84191b1bcb7dc0cfbdaa91a66dead9c13 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Fri, 23 Jun 2023 17:04:22 +0200 Subject: [PATCH 33/63] Disable parallel execution of module that contains DistributionTests (#9952) Specify jetty.ssl.port to avoid clashes with parallel tests Signed-off-by: Ludovic Orban --- .../eclipse/jetty/tests/distribution/DistributionTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 8c9acc3e792..1e3cf778fe1 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -968,10 +968,10 @@ public class DistributionTests extends AbstractJettyHomeTest "\n" + "[ini]\n" + "" + pathProperty + "=modbased\n"; - String moduleName = "ssl-ini"; - Files.writeString(jettyBaseModules.resolve(moduleName + ".mod"), module, StandardOpenOption.CREATE); + Files.writeString(jettyBaseModules.resolve("ssl-ini.mod"), module, StandardOpenOption.CREATE); - try (JettyHomeTester.Run run1 = distribution.start("--add-module=https,test-keystore,ssl-ini")) + int port = distribution.freePort(); + try (JettyHomeTester.Run run1 = distribution.start("--add-module=https,test-keystore,ssl-ini", "jetty.ssl.port=" + port)) { assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); assertEquals(0, run1.getExitValue()); From 62e6cf2b76419d28c9010a327e202eca77897a47 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 23 Jun 2023 17:21:27 +0200 Subject: [PATCH 34/63] Fix #9953 handled HEAD (#9957) * Fix #9953 handled HEAD Fix #9953 so that if a Handler self handles HEAD by not writing content, then the length checks do not fire if 0 bytes have been written. * updates from review --- .../server/internal/HttpChannelState.java | 16 +++-- .../jetty/server/HttpServerTestBase.java | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) 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 d5277662284..f1673add886 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 @@ -1176,16 +1176,15 @@ public class HttpChannelState implements HttpChannel, Components { long length = BufferUtil.length(content); - long totalWritten; HttpChannelState httpChannelState; HttpStream stream = null; - Throwable failure = null; + Throwable failure; MetaData.Response responseMetaData = null; try (AutoLock ignored = _request._lock.lock()) { httpChannelState = _request.lockedGetHttpChannelState(); long committedContentLength = httpChannelState._committedContentLength; - totalWritten = _contentBytesWritten + length; + long totalWritten = _contentBytesWritten + length; long contentLength = committedContentLength >= 0 ? committedContentLength : getHeaders().getLongField(HttpHeader.CONTENT_LENGTH); if (_writeCallback != null) @@ -1193,11 +1192,14 @@ public class HttpChannelState implements HttpChannel, Components else { failure = getFailure(httpChannelState); - if (failure == null && contentLength >= 0) + if (failure == null && contentLength >= 0 && totalWritten != contentLength) { // If the content length were not compatible with what was written, then we need to abort. - String lengthError = (totalWritten > contentLength) ? "written %d > %d content-length" - : (last && totalWritten < contentLength) ? "written %d < %d content-length" : null; + String lengthError = null; + if (totalWritten > contentLength) + lengthError = "written %d > %d content-length"; + else if (last && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod()))) + lengthError = "written %d < %d content-length"; if (lengthError != null) { String message = lengthError.formatted(totalWritten, contentLength); @@ -1439,7 +1441,7 @@ public class HttpChannelState implements HttpChannel, Components long totalWritten = response._contentBytesWritten; long committedContentLength = httpChannelState._committedContentLength; - if (committedContentLength >= 0 && committedContentLength != totalWritten) + if (committedContentLength >= 0 && committedContentLength != totalWritten && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod()))) failure = new IOException("content-length %d != %d written".formatted(committedContentLength, totalWritten)); // is the request fully consumed? diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java index f95dfeae659..7748d93ebed 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java @@ -36,6 +36,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.awaitility.Awaitility; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.io.AbstractConnection; @@ -1334,6 +1335,75 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture } } + @Test + public void testHeadHandled() throws Exception + { + startServer(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.getHeaders().put(HttpHeader.CONTENT_LENGTH, 10); + if (HttpMethod.HEAD.is(request.getMethod())) + { + if (request.getHttpURI().getCanonicalPath().equals("/writeNull")) + response.write(true, null, callback); + else + callback.succeeded(); + } + else + { + Content.Sink.write(response, true, "123456789\n", callback); + } + return true; + } + }); + _httpConfiguration.setSendDateHeader(false); + _httpConfiguration.setSendServerVersion(false); + _httpConfiguration.setSendXPoweredBy(false); + + try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort())) + { + OutputStream os = client.getOutputStream(); + InputStream is = client.getInputStream(); + + os.write(""" + GET / HTTP/1.1 + Host: localhost + + HEAD / HTTP/1.1 + Host: localhost + + HEAD /writeNull HTTP/1.1 + Host: localhost + + GET / HTTP/1.1 + Host: localhost + Connection: close + + """.getBytes(StandardCharsets.ISO_8859_1)); + + String in = IO.toString(is); + assertThat(in.replace("\r", ""), is(""" + HTTP/1.1 200 OK + Content-Length: 10 + + 123456789 + HTTP/1.1 200 OK + Content-Length: 10 + + HTTP/1.1 200 OK + Content-Length: 10 + + HTTP/1.1 200 OK + Content-Length: 10 + Connection: close + + 123456789 + """)); + } + } + @Test public void testBlockedClient() throws Exception { From dd71db35fd36e7db363c09ec11ea4dfd802edc75 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 23 Jun 2023 17:22:29 +0200 Subject: [PATCH 35/63] Fix #9955 zero status (#9956) Fix #9955 zero status is 200 in servlet API --- .../org/eclipse/jetty/ee10/servlet/ServletApiResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 8211dca565f..b4eb67b75af 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 @@ -242,7 +242,8 @@ public class ServletApiResponse implements HttpServletResponse @Override public int getStatus() { - return getResponse().getStatus(); + int status = getResponse().getStatus(); + return status == 0 ? 200 : status; } @Override From 283b74a599bf89a165cbb4576ead01ea1106539f Mon Sep 17 00:00:00 2001 From: Almas Abdrazak Date: Sun, 25 Jun 2023 01:37:34 -0700 Subject: [PATCH 36/63] Fixes #9947 - check that each selector inside of _selectors isn't null before calculating #totalKeys (#9962) Check that selector isn't null before calculating totalKeys. --- .../src/main/java/org/eclipse/jetty/io/SelectorManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java index ad35aa2f388..fab73091622 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java @@ -148,14 +148,14 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump public int getTotalKeys() { int keys = 0; - for (final ManagedSelector selector : _selectors) + for (ManagedSelector selector : _selectors) { - keys += selector.getTotalKeys(); + if (selector != null) + keys += selector.getTotalKeys(); } return keys; } - /** * @return the number of selectors in use */ From bf2ef887b8d1da0280cfc9ad59ccf6da546559d9 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Mon, 26 Jun 2023 09:32:42 +0200 Subject: [PATCH 37/63] Specify jetty.ssl.port to the command that actually executes jetty Signed-off-by: Ludovic Orban --- .../eclipse/jetty/tests/distribution/DistributionTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 1e3cf778fe1..67e9fb0113c 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -970,14 +970,14 @@ public class DistributionTests extends AbstractJettyHomeTest "" + pathProperty + "=modbased\n"; Files.writeString(jettyBaseModules.resolve("ssl-ini.mod"), module, StandardOpenOption.CREATE); - int port = distribution.freePort(); - try (JettyHomeTester.Run run1 = distribution.start("--add-module=https,test-keystore,ssl-ini", "jetty.ssl.port=" + port)) + try (JettyHomeTester.Run run1 = distribution.start("--add-module=https,test-keystore,ssl-ini")) { assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); assertEquals(0, run1.getExitValue()); // Override the property on the command line with the correct password. - try (JettyHomeTester.Run run2 = distribution.start(pathProperty + "=cmdline")) + int port = distribution.freePort(); + try (JettyHomeTester.Run run2 = distribution.start(pathProperty + "=cmdline", "jetty.ssl.port=" + port)) { assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); assertThat("${jetty.base}/cmdline", jettyBase.resolve("cmdline"), PathMatchers.isRegularFile()); From f2aee378a38447e8aa61572440bd696001d9de25 Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Mon, 26 Jun 2023 12:47:58 +0200 Subject: [PATCH 38/63] ensure DemoModulesTest use random port for ssl connector (#9969) Signed-off-by: Olivier Lamy --- .../tests/distribution/DemoModulesTests.java | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java index 3f81cdf6881..de2bfaefe1a 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java @@ -53,7 +53,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest @MethodSource("provideEnvironmentsToTest") public void testAuthentication(String env) throws Exception { - Path jettyBase = newTestJettyBaseDirectory(); + Path jettyBase = newTestJettyBaseDirectory(); String jettyVersion = System.getProperty("jettyVersion"); JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() .jettyVersion(jettyVersion) @@ -62,6 +62,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http," + toEnvironment("demos", env) @@ -75,7 +76,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) @@ -149,6 +151,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http," + toEnvironment("demo-jsp", env) @@ -162,7 +165,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest assertEquals(0, runConfig.getExitValue()); String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) @@ -194,6 +198,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http," + toEnvironment("demo-jaas", env) @@ -208,7 +213,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) @@ -240,6 +246,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http," + toEnvironment("demo-jsp", env) @@ -253,7 +260,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest assertEquals(0, runConfig.getExitValue()); String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) @@ -287,6 +295,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http," + toEnvironment("demo-async-rest", env) @@ -300,7 +309,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest assertEquals(0, runConfig.getExitValue()); String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) @@ -342,6 +352,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http," + toEnvironment("demos", env) @@ -353,7 +364,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest assertEquals(0, runConfig.getExitValue()); String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) @@ -397,6 +409,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http," + toEnvironment("demos", env) @@ -411,7 +424,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest String[] argsStart = { "--jpms", - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) { @@ -453,8 +467,10 @@ public class DemoModulesTests extends AbstractJettyHomeTest assertEquals(0, runConfig.getExitValue()); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) { @@ -519,8 +535,11 @@ public class DemoModulesTests extends AbstractJettyHomeTest assertEquals(0, runConfig.getExitValue()); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); + String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) { @@ -552,6 +571,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { "--add-modules=http,demo-handler" @@ -565,7 +585,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest assertEquals(0, runConfig.getExitValue()); String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) @@ -594,6 +615,7 @@ public class DemoModulesTests extends AbstractJettyHomeTest .build(); int httpPort = distribution.freePort(); + int sslPort = distribution.freePort(); String[] argsConfig = { @@ -607,7 +629,8 @@ public class DemoModulesTests extends AbstractJettyHomeTest String[] argsStart = { - "jetty.http.port=" + httpPort + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort }; try (JettyHomeTester.Run runStart = distribution.start(argsStart)) { From 3a5136834f03e71ec5e8afd080a697e953dd844d Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 26 Jun 2023 16:16:35 +0200 Subject: [PATCH 39/63] Deflake ReservedThreadExecutorTest (#9968) * Deflake ReservedThreadExecutorTest Use awaitability with longer times to avoid short optimistic waits in tests. * Deflake ReservedThreadExecutorTest Use awaitability with longer times to avoid short optimistic waits in tests. --- .../thread/ReservedThreadExecutorTest.java | 63 +++++++------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java index 4d07212307a..a3f954a44f1 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java @@ -17,22 +17,22 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import org.eclipse.jetty.util.NanoTime; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.waitAtMost; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; public class ReservedThreadExecutorTest { @@ -93,7 +93,7 @@ public class ReservedThreadExecutorTest } assertThat(_executor._queue.size(), is(0)); - waitForAllAvailable(); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); for (int i = 0; i < SIZE; i++) { @@ -127,7 +127,7 @@ public class ReservedThreadExecutorTest } assertThat(_executor._queue.size(), is(0)); - waitForAllAvailable(); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); Task[] tasks = new Task[SIZE]; for (int i = 0; i < SIZE; i++) @@ -138,7 +138,7 @@ public class ReservedThreadExecutorTest for (int i = 0; i < SIZE; i++) { - tasks[i]._ran.await(10, TimeUnit.SECONDS); + tasks[i]._ran.await(10, SECONDS); } assertThat(_executor._queue.size(), is(1)); @@ -147,15 +147,12 @@ public class ReservedThreadExecutorTest assertThat(_reservedExecutor.tryExecute(extra), is(false)); assertThat(_executor._queue.size(), is(2)); - Thread.sleep(500); - assertThat(extra._ran.getCount(), is(1L)); + waitAtMost(5, SECONDS).until(extra._ran::getCount, is(1L)); for (int i = 0; i < SIZE; i++) - { tasks[i]._complete.countDown(); - } - waitForAllAvailable(); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); } @Test @@ -164,7 +161,7 @@ public class ReservedThreadExecutorTest final long IDLE = 1000; _reservedExecutor.stop(); - _reservedExecutor.setIdleTimeout(IDLE, TimeUnit.MILLISECONDS); + _reservedExecutor.setIdleTimeout(IDLE, MILLISECONDS); _reservedExecutor.start(); assertThat(_reservedExecutor.getAvailable(), is(0)); @@ -174,12 +171,12 @@ public class ReservedThreadExecutorTest _executor.startThread(); _executor.startThread(); - waitForAvailable(2); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(2)); int available = _reservedExecutor.getAvailable(); assertThat(available, is(2)); - Thread.sleep((5 * IDLE) / 2); - assertThat(_reservedExecutor.getAvailable(), is(0)); + + waitAtMost(5 * IDLE, MILLISECONDS).until(_reservedExecutor::getAvailable, is(0)); } @Test @@ -188,7 +185,7 @@ public class ReservedThreadExecutorTest final long IDLE = 1000; _reservedExecutor.stop(); - _reservedExecutor.setIdleTimeout(IDLE, TimeUnit.MILLISECONDS); + _reservedExecutor.setIdleTimeout(IDLE, MILLISECONDS); _reservedExecutor.start(); assertThat(_reservedExecutor.getAvailable(), is(0)); @@ -198,7 +195,7 @@ public class ReservedThreadExecutorTest _executor.startThread(); _executor.startThread(); - waitForAvailable(2); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(2)); int available = _reservedExecutor.getAvailable(); assertThat(available, is(2)); @@ -206,7 +203,7 @@ public class ReservedThreadExecutorTest for (int i = 10; i-- > 0;) { assertThat(_reservedExecutor.tryExecute(NOOP), is(true)); - Thread.sleep(200); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, greaterThan(0)); } assertThat(_reservedExecutor.getAvailable(), is(1)); } @@ -216,38 +213,20 @@ public class ReservedThreadExecutorTest { long idleTimeout = 500; _reservedExecutor.stop(); - _reservedExecutor.setIdleTimeout(idleTimeout, TimeUnit.MILLISECONDS); + _reservedExecutor.setIdleTimeout(idleTimeout, MILLISECONDS); _reservedExecutor.start(); assertThat(_reservedExecutor.tryExecute(NOOP), is(false)); Thread thread = _executor.startThread(); assertNotNull(thread); - waitForAvailable(1); - - Thread.sleep(2 * idleTimeout); - - waitForAvailable(0); + // watching the available thread count decrease relies on the waitAtMost sampling rate being + // faster than the idleTimeout, so it always sees the 1 before the 0 + waitAtMost(5, SECONDS).until(_reservedExecutor::getAvailable, is(1)); + waitAtMost(5, SECONDS).until(_reservedExecutor::getAvailable, is(0)); thread.join(2 * idleTimeout); assertFalse(thread.isAlive()); } - protected void waitForAvailable(int size) throws InterruptedException - { - long started = NanoTime.now(); - while (_reservedExecutor.getAvailable() < size) - { - if (NanoTime.secondsSince(started) > 10) - fail("Took too long"); - Thread.sleep(10); - } - assertThat(_reservedExecutor.getAvailable(), is(size)); - } - - protected void waitForAllAvailable() throws InterruptedException - { - waitForAvailable(SIZE); - } - private static class TestExecutor implements Executor { private final Deque _queue = new ArrayDeque<>(); @@ -350,7 +329,7 @@ public class ReservedThreadExecutorTest task.run(); task.run(); - assertTrue(executed.await(60, TimeUnit.SECONDS)); + assertTrue(executed.await(60, SECONDS)); // ensure tryExecute is still working while (!reserved.tryExecute(() -> {})) From 939689b6695cd6d1f42cdd826b1c5adc694fc705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Nie=C3=9Fing?= <36885591+zUniQueX@users.noreply.github.com> Date: Mon, 26 Jun 2023 18:24:13 +0200 Subject: [PATCH 40/63] Spotbugs fixes (#9961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add zone id to DateTimeFormatter Inline the call to withZone(ZoneId) because it returns a new DateTimeFormatter instead of modifying the current one in-place Signed-off-by: Steffen Nießing * Remove redundant null-check The array already was null-checked, so remove the redundant check Signed-off-by: Steffen Nießing * Cleanup ArrayUtil ArrayUtil provides static methods only -> Prevent instantiation and remove unused interfaces Signed-off-by: Steffen Nießing * Remove dead assignment The charset variable isn't accessed after the assignment anymore Signed-off-by: Steffen Nießing * Prevent instantiation TypeUtil contains static members only and should not be instantiated Signed-off-by: Steffen Nießing * Remove redundant null-check The loader variable is checked to be non-null before Signed-off-by: Steffen Nießing * Restrict charset to ascii The JavaDoc describes the byte array to contain ascii characters only Signed-off-by: Steffen Nießing * Prevent instantiation StringUtil contains static members only Signed-off-by: Steffen Nießing * Prevent instantiation IO contains static members only Signed-off-by: Steffen Nießing --------- Signed-off-by: Steffen Nießing --- .../org/eclipse/jetty/logging/Timestamp.java | 5 +++-- .../org/eclipse/jetty/util/ArrayUtil.java | 9 ++++++--- .../org/eclipse/jetty/util/DateCache.java | 11 +++++++---- .../main/java/org/eclipse/jetty/util/IO.java | 5 +++++ .../org/eclipse/jetty/util/StringUtil.java | 5 +++++ .../java/org/eclipse/jetty/util/TypeUtil.java | 19 +++++++++++-------- .../org/eclipse/jetty/util/UrlEncoded.java | 1 - .../jetty/util/jmh/DateCacheNoTick.java | 11 +++++++---- 8 files changed, 44 insertions(+), 22 deletions(-) diff --git a/jetty-core/jetty-slf4j-impl/src/main/java/org/eclipse/jetty/logging/Timestamp.java b/jetty-core/jetty-slf4j-impl/src/main/java/org/eclipse/jetty/logging/Timestamp.java index c0be0909967..92a8c3cb7f4 100644 --- a/jetty-core/jetty-slf4j-impl/src/main/java/org/eclipse/jetty/logging/Timestamp.java +++ b/jetty-core/jetty-slf4j-impl/src/main/java/org/eclipse/jetty/logging/Timestamp.java @@ -51,9 +51,10 @@ class Timestamp public Timestamp(TimeZone timeZone) { - tzFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); zoneId = timeZone.toZoneId(); - tzFormatter.withZone(zoneId); + tzFormatter = DateTimeFormatter + .ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(zoneId); tick = null; } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayUtil.java index dedb84652d7..ea60144ea83 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayUtil.java @@ -13,7 +13,6 @@ package org.eclipse.jetty.util; -import java.io.Serializable; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; @@ -23,7 +22,6 @@ import java.util.List; * Utility methods for Array manipulation */ public class ArrayUtil - implements Cloneable, Serializable { public static T[] removeFromArray(T[] array, Object item) @@ -34,7 +32,7 @@ public class ArrayUtil { if (item.equals(array[i])) { - Class c = array == null ? item.getClass() : array.getClass().getComponentType(); + Class c = array.getClass().getComponentType(); @SuppressWarnings("unchecked") T[] na = (T[])Array.newInstance(c, Array.getLength(array) - 1); if (i > 0) @@ -155,5 +153,10 @@ public class ArrayUtil } return array; } + + private ArrayUtil() + { + // prevents instantiation + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java index 65c4f476b78..d5b6dc09085 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java @@ -132,16 +132,19 @@ public class DateCache else _tzFormatString = _formatString; + _zoneId = tz.toZoneId(); if (_locale != null) { - _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString, _locale); + _tzFormat = DateTimeFormatter + .ofPattern(_tzFormatString, _locale) + .withZone(_zoneId); } else { - _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString); + _tzFormat = DateTimeFormatter + .ofPattern(_tzFormatString) + .withZone(_zoneId); } - _zoneId = tz.toZoneId(); - _tzFormat.withZone(_zoneId); _tick = null; } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IO.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IO.java index d9ad3ca6322..28bb4ee71e3 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IO.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IO.java @@ -536,6 +536,11 @@ public class IO return null; } + + private IO() + { + // prevent instantiation + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index 4680976aa22..c68c4ecaf71 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -1254,6 +1254,11 @@ public class StringUtil .collect(StringBuilder::new, (b, c) -> b.append((char)c), StringBuilder::append) .toString(); } + + private StringUtil() + { + // prevent instantiation + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java index ce712467dec..fe0594dda7c 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java @@ -25,6 +25,7 @@ import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; @@ -406,7 +407,7 @@ public class TypeUtil digit = 10 + c - 'a'; } if (digit < 0 || digit >= base) - throw new NumberFormatException(new String(b, offset, length)); + throw new NumberFormatException(new String(b, offset, length, StandardCharsets.US_ASCII)); value = value * base + digit; } return value; @@ -671,14 +672,11 @@ public class TypeUtil try { String resourceName = TypeUtil.toClassReference(clazz); - if (loader != null) + URL url = loader.getResource(resourceName); + if (url != null) { - URL url = loader.getResource(resourceName); - if (url != null) - { - URI uri = url.toURI(); - return URIUtil.unwrapContainer(uri); - } + URI uri = url.toURI(); + return URIUtil.unwrapContainer(uri); } } catch (URISyntaxException ignored) @@ -819,4 +817,9 @@ public class TypeUtil { return StreamSupport.stream(new ServiceLoaderSpliterator<>(serviceLoader), false); } + + private TypeUtil() + { + // prevents instantiation + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java index 07cd8668e59..1c8632afdbf 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java @@ -60,7 +60,6 @@ public class UrlEncoded charset = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset"); if (charset == null) { - charset = StandardCharsets.UTF_8.toString(); encoding = StandardCharsets.UTF_8; } else diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheNoTick.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheNoTick.java index ee34b661e4f..e90866e9ebb 100644 --- a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheNoTick.java +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheNoTick.java @@ -118,16 +118,19 @@ public class DateCacheNoTick else _tzFormatString = _formatString; + _zoneId = tz.toZoneId(); if (_locale != null) { - _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString, _locale); + _tzFormat = DateTimeFormatter + .ofPattern(_tzFormatString, _locale) + .withZone(_zoneId); } else { - _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString); + _tzFormat = DateTimeFormatter + .ofPattern(_tzFormatString) + .withZone(_zoneId); } - _zoneId = tz.toZoneId(); - _tzFormat.withZone(_zoneId); } public TimeZone getTimeZone() From cbd83c91c25636736db7a358c929ecb4b042938e Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Mon, 26 Jun 2023 22:09:17 +0200 Subject: [PATCH 41/63] Fix default servlet character encoding directories (#9970) #9966 adapt character encoding when including a path that is a directory listing Signed-off-by: Ludovic Orban --- .../eclipse/jetty/server/ResourceListing.java | 5 +- .../eclipse/jetty/server/ResourceService.java | 7 +- .../jetty/ee10/servlet/DefaultServlet.java | 53 +++++++-- .../ee10/servlet/DefaultServletTest.java | 106 +++++++++++++++++- 4 files changed, 155 insertions(+), 16 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceListing.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceListing.java index 61bb9b2225c..66d3a635e55 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceListing.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceListing.java @@ -51,7 +51,7 @@ public class ResourceListing * @param base The base URL * @param parent True if the parent directory should be included * @param query query params - * @return the HTML as String + * @return the XHTML as String */ public static String getAsXHTML(Resource resource, String base, boolean parent, String query) { @@ -108,7 +108,8 @@ public class ResourceListing StringBuilder buf = new StringBuilder(4096); - // Doctype Declaration + XHTML + // Doctype Declaration + XHTML. The spec says the encoding MUST be "utf-8" in all cases at it is ignored; + // see: https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-charset buf.append(""" 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 48d998ddf6a..b737459b939 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 @@ -16,6 +16,7 @@ package org.eclipse.jetty.server; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; @@ -626,8 +627,10 @@ public class ResourceService return; } - byte[] data = listing.getBytes(StandardCharsets.UTF_8); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;charset=utf-8"); + String characterEncoding = httpContent.getCharacterEncoding(); + Charset charset = characterEncoding == null ? StandardCharsets.UTF_8 : Charset.forName(characterEncoding); + byte[] data = listing.getBytes(charset); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;charset=" + charset.name()); response.getHeaders().put(HttpHeader.CONTENT_LENGTH, data.length); response.write(true, ByteBuffer.wrap(data), callback); } 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 73e880a889f..22dc3cce438 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 @@ -490,7 +490,7 @@ public class DefaultServlet extends HttpServlet else { ServletCoreRequest coreRequest = new ServletCoreRequest(req); - ServletCoreResponse coreResponse = new ServletCoreResponse(coreRequest, resp); + ServletCoreResponse coreResponse = new ServletCoreResponse(coreRequest, resp, included); if (coreResponse.isCommitted()) { @@ -861,13 +861,34 @@ public class DefaultServlet extends HttpServlet private final ServletCoreRequest _coreRequest; private final Response _coreResponse; private final HttpFields.Mutable _httpFields; + private final boolean _included; - public ServletCoreResponse(ServletCoreRequest coreRequest, HttpServletResponse response) + public ServletCoreResponse(ServletCoreRequest coreRequest, HttpServletResponse response, boolean included) { _coreRequest = coreRequest; _response = response; _coreResponse = ServletContextResponse.getServletContextResponse(response); - _httpFields = new HttpServletResponseHttpFields(response); + HttpFields.Mutable fields = new HttpServletResponseHttpFields(response); + if (included) + { + // If included, accept but ignore mutations. + fields = new HttpFields.Mutable.Wrapper(fields) + { + @Override + public HttpField onAddField(HttpField field) + { + return null; + } + + @Override + public boolean onRemoveField(HttpField field) + { + return false; + } + }; + } + _httpFields = fields; + _included = included; } @Override @@ -918,6 +939,8 @@ public class DefaultServlet extends HttpServlet @Override public void write(boolean last, ByteBuffer byteBuffer, Callback callback) { + if (_included) + last = false; try { if (BufferUtil.hasContent(byteBuffer)) @@ -967,6 +990,8 @@ public class DefaultServlet extends HttpServlet { if (LOG.isDebugEnabled()) LOG.debug("{}.setStatus({})", this.getClass().getSimpleName(), code); + if (_included) + return; _response.setStatus(code); } @@ -1151,7 +1176,8 @@ public class DefaultServlet extends HttpServlet HttpServletResponse response = getServletResponse(coreResponse); try { - // TODO: not sure if this is allowed here. + if (isIncluded(request)) + return; if (cause != null) request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, cause); response.sendError(statusCode, reason); @@ -1231,13 +1257,20 @@ public class DefaultServlet extends HttpServlet public ForcedCharacterEncodingHttpContent(HttpContent content, String characterEncoding) { - super(content); + super(Objects.requireNonNull(content)); this.characterEncoding = characterEncoding; - String mimeType = content.getContentTypeValue(); - int idx = mimeType.indexOf(";charset"); - if (idx >= 0) - mimeType = mimeType.substring(0, idx); - this.contentType = mimeType + ";charset=" + this.characterEncoding; + if (content.getContentTypeValue() == null || content.getResource().isDirectory()) + { + this.contentType = null; + } + else + { + String mimeType = content.getContentTypeValue(); + int idx = mimeType.indexOf(";charset"); + if (idx >= 0) + mimeType = mimeType.substring(0, idx); + this.contentType = mimeType + ";charset=" + characterEncoding; + } } @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java index 213a4399634..0ab4338edd9 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; @@ -62,6 +63,7 @@ import org.eclipse.jetty.toolchain.test.MavenPaths; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.junit.jupiter.api.AfterEach; @@ -377,6 +379,101 @@ public class DefaultServletTest assertThat(body, containsString("f??r")); } + @Test + public void testSimpleListing() throws Exception + { + ServletHolder defHolder = context.addServlet(DefaultServlet.class, "/*"); + defHolder.setInitParameter("dirAllowed", "true"); + + String rawResponse = connector.getResponse(""" + GET /context/ HTTP/1.1\r + Host: local\r + Connection: close\r + \r + """); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat(response.getStatus(), is(200)); + assertThat(response.getField("content-type").getValue(), is("text/html;charset=UTF-8")); + String body = response.getContent(); + assertThat(body, containsString("")); + } + + @Test + public void testIncludeListingAllowed() throws Exception + { + ServletHolder defHolder = context.addServlet(DefaultServlet.class, "/*"); + defHolder.setInitParameter("dirAllowed", "true"); + + /* create a file with a non-fully ASCII name in the docroot */ + Files.writeString(docRoot.resolve("numéros-en-français.txt"), "un deux trois", StandardCharsets.ISO_8859_1); + + ServletHolder incHolder = new ServletHolder(); + incHolder.setInstance(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + // Getting the writer implicitly sets the charset to iso-8859-1. + resp.getWriter().println(">>>"); + resp.getWriter().println("éèàîû"); + req.getRequestDispatcher("/").include(req, resp); + resp.getWriter().println("<<<"); + } + }); + context.addServlet(incHolder, "/inclusion"); + + String rawResponse = connector.getResponse(""" + GET /context/inclusion HTTP/1.1\r + Host: local\r + Connection: close\r + \r + """); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat(response.getStatus(), is(200)); + String body = BufferUtil.toString(response.getContentByteBuffer(), StandardCharsets.ISO_8859_1); + assertThat(body, startsWith(">>>\néèàîû\n")); + assertThat(body, containsString("")); + assertThat(body, containsString("numéros-en-français.txt")); + assertThat(body, endsWith("<<<\n")); + } + + @Test + public void testIncludeListingForbidden() throws Exception + { + ServletHolder defHolder = context.addServlet(DefaultServlet.class, "/*"); + defHolder.setInitParameter("dirAllowed", "false"); + + ServletHolder incHolder = new ServletHolder(); + incHolder.setInstance(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + // Getting the writer implicitly sets the charset to iso-8859-1. + resp.getWriter().println(">>>"); + req.getRequestDispatcher("/").include(req, resp); + resp.getWriter().println("éèàîû"); + resp.getWriter().println("<<<"); + } + }); + context.addServlet(incHolder, "/inclusion"); + + String rawResponse = connector.getResponse(""" + GET /context/inclusion HTTP/1.1\r + Host: local\r + Connection: close\r + \r + """); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat(response.getStatus(), is(200)); + assertThat(response.get(HttpHeader.CONTENT_LENGTH), is("14")); + String body = BufferUtil.toString(response.getContentByteBuffer(), StandardCharsets.ISO_8859_1); + assertThat(body, is(">>>\néèàîû\n<<<\n")); + } + /** * A regression on windows allowed the directory listing show * the fully qualified paths within the directory listing. @@ -1178,7 +1275,11 @@ public class DefaultServletTest */ assertThat(response.toString(), response.getStatus(), is(HttpStatus.INTERNAL_SERVER_ERROR_500)); - // This resource does exist but directory listings are not allowed and there are no welcome files. + /* This resource does exist but directory listings are not allowed and there are no welcome files; + * and since RequestDispatcher#include(ServletRequest, ServletResponse) says: + * "The included servlet cannot change the response status code or set headers; any attempt to make a change is ignored." + * the response status should be 200 and there should be no content. + */ rawResponse = connector.getResponse(""" GET /context/gateway?includeTarget=/alt/ HTTP/1.1\r Host: local\r @@ -1186,7 +1287,8 @@ public class DefaultServletTest \r """); response = HttpTester.parseResponse(rawResponse); - assertThat(response.toString(), response.getStatus(), is(HttpStatus.FORBIDDEN_403)); + assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.toString(), containsString("Content-Length: 0")); // Once index.html has been created we can include this same target and see it as a welcome file. Files.writeString(altRoot.resolve("index.html"), "

    Alt Index

    ", UTF_8); From 8b0e28f7b51fb34efbc097c300ea40e7b6f7d5f3 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 27 Jun 2023 13:07:02 +1000 Subject: [PATCH 42/63] update javadoc for nested ContextHandler Signed-off-by: Lachlan Roberts --- .../jetty/ee9/nested/ContextHandler.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 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 4025e4c385e..db70e7398dc 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 @@ -72,6 +72,7 @@ import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Session; +import org.eclipse.jetty.server.handler.ContextHandler.ScopedContext; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.ContextRequest; import org.eclipse.jetty.session.AbstractSessionManager; @@ -118,6 +119,20 @@ import org.slf4j.LoggerFactory; * By default, the context is created with the {@link AllowedResourceAliasChecker} which is configured to allow symlinks. If * this alias checker is not required, then {@link #clearAliasChecks()} or {@link #setAliasChecks(List)} should be called. *

    + * This handler can be invoked in 2 different ways: + *
      + *
    • + * If this is added directly as a {@link Handler} on the {@link Server} this will supply the {@link CoreContextHandler} + * associated with this {@link ContextHandler}. This will wrap the request to a {@link CoreContextRequest} and fall + * through to the {@code CoreToNestedHandler} which invokes the {@link HttpChannel} and this will eventually reach + * {@link ContextHandler#handle(String, Request, HttpServletRequest, HttpServletResponse)}. + *
    • + *
    • + * If this is nested inside another {@link ContextHandler} and not added directly to the server then its + * {@link CoreContextHandler} will never be added to the server. However it will still be created and its + * {@link ScopedContext} will be used to enter scope. + *
    • + *
    */ @ManagedObject("EE9 Context") public class ContextHandler extends ScopedHandler implements Attributes, Supplier @@ -901,7 +916,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie baseRequest.setContext(_apiContext, (DispatcherType.INCLUDE.equals(dispatch) || !target.startsWith("/")) ? oldPathInContext : pathInContext); - org.eclipse.jetty.server.handler.ContextHandler.ScopedContext context = getCoreContextHandler().getContext(); + ScopedContext context = getCoreContextHandler().getContext(); if (context == org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext()) { nextScope(target, baseRequest, request, response); @@ -1766,7 +1781,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie */ public class APIContext implements ServletContext { - private final org.eclipse.jetty.server.handler.ContextHandler.ScopedContext _coreContext; + private final ScopedContext _coreContext; protected boolean _enabled = true; // whether or not the dynamic API is enabled for callers protected boolean _extendedListenerTypes = false; private int _effectiveMajorVersion = SERVLET_MAJOR_VERSION; @@ -2417,7 +2432,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie AbstractSessionManager.RequestedSession _requestedSession; protected CoreContextRequest(org.eclipse.jetty.server.Request wrapped, - org.eclipse.jetty.server.handler.ContextHandler.ScopedContext context, + ScopedContext context, HttpChannel httpChannel) { super(context, wrapped); From 2b4e8960e6828f635c7ea0b5767969408ada9ade Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 27 Jun 2023 13:51:22 +1000 Subject: [PATCH 43/63] #9945 - update javadoc to say demand() can throw ReadPendingException Signed-off-by: Lachlan Roberts --- .../main/java/org/eclipse/jetty/websocket/core/CoreSession.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java index 29563420585..c9f6bae28b3 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/CoreSession.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.websocket.core; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; +import java.nio.channels.ReadPendingException; import java.util.List; import java.util.Map; @@ -155,6 +156,7 @@ public interface CoreSession extends OutgoingFrames, IncomingFrames, Configurati *

    Manages flow control by indicating demand for a WebSocket frame.

    *

    A call to {@link FrameHandler#onFrame(Frame, Callback)} will only * be made if there is demand.

    + *

    If a previous demand has not been fulfilled this will throw {@link ReadPendingException}

    * * {@link FrameHandler#onFrame(Frame, Callback)}. */ From 07099466752fa668a06f9e57a922f506f802db5e Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 27 Jun 2023 14:09:41 +1000 Subject: [PATCH 44/63] Issue #9965 - make multiple websocket demand throw ISE Signed-off-by: Lachlan Roberts --- .../websocket/core/WebSocketConnection.java | 26 +++++++++---------- .../websocket/core/util/DemandingFlusher.java | 14 +++++----- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java index 11909455d2e..785fcd1a686 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; +import java.nio.channels.ReadPendingException; import java.security.SecureRandom; import java.util.Objects; import java.util.Random; @@ -31,7 +32,6 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.MathUtils; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.Scheduler; @@ -61,7 +61,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio private final WebSocketCoreSession coreSession; private final Flusher flusher; private final Random random; - private long demand; + private Boolean demand; private boolean fillingAndParsing; private final LongAdder messagesIn = new LongAdder(); private final LongAdder bytesIn = new LongAdder(); @@ -351,10 +351,12 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (LOG.isDebugEnabled()) LOG.debug("demand {} d={} fp={} {}", demand, fillingAndParsing, networkBuffer, this); - if (demand < 0) - return; - - demand = MathUtils.cappedAdd(demand, 1); + if (demand != null) + { + if (demand) + throw new ReadPendingException(); + demand = true; + } if (!fillingAndParsing) { @@ -379,13 +381,12 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (!fillingAndParsing) throw new IllegalStateException(); - if (demand != 0) //if demand was canceled, this creates synthetic demand in order to read until EOF + if (demand == null) // If demand was canceled, this creates synthetic demand in order to read until EOF. return true; fillingAndParsing = false; if (!networkBuffer.hasRemaining()) releaseNetworkBuffer(); - return false; } } @@ -397,14 +398,13 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (LOG.isDebugEnabled()) LOG.debug("meetDemand d={} fp={} {} {}", demand, fillingAndParsing, networkBuffer, this); - if (demand == 0) + if (demand == Boolean.FALSE) throw new IllegalStateException(); if (!fillingAndParsing) throw new IllegalStateException(); - if (demand > 0) - demand--; - + if (demand != null) + demand = false; return true; } } @@ -415,7 +415,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio { if (LOG.isDebugEnabled()) LOG.debug("cancelDemand d={} fp={} {} {}", demand, fillingAndParsing, networkBuffer, this); - demand = -1; + demand = null; } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java index be55fc88d42..6b70fd120a4 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java @@ -13,7 +13,8 @@ package org.eclipse.jetty.websocket.core.util; -import java.util.concurrent.atomic.AtomicLong; +import java.nio.channels.ReadPendingException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.util.Callback; @@ -41,7 +42,7 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema private static final Throwable SENTINEL_CLOSE_EXCEPTION = new StaticException("Closed"); private final IncomingFrames _emitFrame; - private final AtomicLong _demand = new AtomicLong(); + private final AtomicBoolean _demand = new AtomicBoolean(); private final AtomicReference _failure = new AtomicReference<>(); private DemandChain _nextDemand; @@ -77,7 +78,8 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema @Override public void demand() { - _demand.incrementAndGet(); + if (!_demand.compareAndSet(false, true)) + throw new ReadPendingException(); iterate(); } @@ -139,8 +141,8 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema */ public void emitFrame(Frame frame, Callback callback) { - if (_demand.decrementAndGet() < 0) - throw new IllegalStateException("Negative Demand"); + if (_demand.compareAndSet(true, false)) + throw new IllegalStateException("Demand Already Fulfilled"); _emitFrame.onFrame(frame, callback); } @@ -153,7 +155,7 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema if (failure != null) throw failure; - if (_demand.get() <= 0) + if (!_demand.get()) break; if (_needContent) From 54dbd9515a86bed7a042786def84f2915e553343 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Tue, 27 Jun 2023 07:25:22 -0500 Subject: [PATCH 45/63] Adding test to prove out zipfs bug with backslash character --- .../resource/MountedPathResourceTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java index 577a1e3d49e..b2e09c2e0b9 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -318,6 +319,28 @@ public class MountedPathResourceTest } } + /** + * Test resolving a resource that has a backslash + */ + @Test + @Disabled("Test disabled due to a JDK bug") + public void testJarFileResourceAccessBackSlash() throws Exception + { + Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); + URI uri = URI.create("jar:" + testJar.toUri().toASCIIString() + "!/"); + try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable()) + { + Resource resource = resourceFactory.newResource(uri); + Resource oddities = resource.resolve("rez/oddities/"); + + assertThat("path /rez/oddities/ is a dir", oddities.isDirectory(), is(true)); + + // attempt to access a resource with a backslash + Resource someSlash = oddities.resolve("some\\slash\\you\\got\\there"); + assertTrue(Resources.isReadableFile(someSlash), "someSlash is accessible as a file"); + } + } + @Test public void testJarFileResourceListDirWithSpace() throws Exception { From c942a918d9bbbbc91170dc5ea72949ed8d28f682 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 28 Jun 2023 14:30:17 +1000 Subject: [PATCH 46/63] fixes for websocket demand changes Signed-off-by: Lachlan Roberts --- .../websocket/core/WebSocketConnection.java | 4 +-- .../websocket/core/util/DemandingFlusher.java | 2 +- .../core/server/WebSocketServerTest.java | 34 ++++++------------- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java index 785fcd1a686..c429bcde0a8 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java @@ -61,7 +61,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio private final WebSocketCoreSession coreSession; private final Flusher flusher; private final Random random; - private Boolean demand; + private Boolean demand = false; private boolean fillingAndParsing; private final LongAdder messagesIn = new LongAdder(); private final LongAdder bytesIn = new LongAdder(); @@ -381,7 +381,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (!fillingAndParsing) throw new IllegalStateException(); - if (demand == null) // If demand was canceled, this creates synthetic demand in order to read until EOF. + if (demand != Boolean.FALSE) // If demand was canceled, this creates synthetic demand in order to read until EOF. return true; fillingAndParsing = false; diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java index 6b70fd120a4..85eac40fd2b 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java @@ -141,7 +141,7 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema */ public void emitFrame(Frame frame, Callback callback) { - if (_demand.compareAndSet(true, false)) + if (!_demand.compareAndSet(true, false)) throw new IllegalStateException("Demand Already Fulfilled"); _emitFrame.onFrame(frame, callback); } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java index 57cc99a7ea6..56f3ac2b1e8 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/server/WebSocketServerTest.java @@ -111,13 +111,12 @@ public class WebSocketServerTest extends WebSocketTester assertNull(frame); serverHandler.getCoreSession().demand(); - serverHandler.getCoreSession().demand(); - frame = serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS); assertNotNull(frame); assertThat(frame.getPayloadAsUTF8(), is("Hello")); client.getOutputStream().write(RawFrameBuilder.buildText("World", true)); + serverHandler.getCoreSession().demand(); frame = serverHandler.receivedFrames.poll(10, TimeUnit.SECONDS); assertNotNull(frame); assertThat(frame.getPayloadAsUTF8(), is("World")); @@ -145,20 +144,13 @@ public class WebSocketServerTest extends WebSocketTester TestFrameHandler serverHandler = new TestFrameHandler() { - @Override - public void onOpen(CoreSession coreSession, Callback callback) - { - super.onOpen(coreSession, callback); - coreSession.demand(); - } - @Override public void onFrame(Frame frame, Callback callback) { LOG.info("onFrame: " + BufferUtil.toDetailString(frame.getPayload())); receivedFrames.offer(frame); receivedCallbacks.offer(callback); - getCoreSession().demand(); + demand(); } }; @@ -246,12 +238,9 @@ public class WebSocketServerTest extends WebSocketTester TestFrameHandler serverHandler = new TestFrameHandler() { @Override - public void onOpen(CoreSession coreSession, Callback callback) + public void onOpen(CoreSession coreSession) { - super.onOpen(coreSession, callback); - coreSession.demand(); - coreSession.demand(); - coreSession.demand(); + super.onOpen(coreSession); } @Override @@ -281,12 +270,13 @@ public class WebSocketServerTest extends WebSocketTester client.getOutputStream().write(BufferUtil.toArray(buffer)); long start = NanoTime.now(); - while (serverHandler.receivedFrames.size() < 3) + for (int i = 0; i < 3; i++) { - assertThat(NanoTime.secondsSince(start), Matchers.lessThan(10L)); - Thread.sleep(10); + serverHandler.getCoreSession().demand(); + Frame frame = serverHandler.receivedFrames.poll(5, TimeUnit.SECONDS); + assertNotNull(frame); } - assertThat(serverHandler.receivedFrames.size(), is(3)); + assertThat(NanoTime.secondsSince(start), Matchers.lessThan(10L)); assertThat(receivedCallbacks.size(), is(3)); client.close(); @@ -310,8 +300,6 @@ public class WebSocketServerTest extends WebSocketTester { super.onOpen(coreSession); callback.succeeded(); - coreSession.demand(); - coreSession.demand(); } @Override @@ -327,6 +315,7 @@ public class WebSocketServerTest extends WebSocketTester LOG.info("onFrame: " + BufferUtil.toDetailString(frame.getPayload())); receivedFrames.offer(frame); receivedCallbacks.offer(callback); + demand(); } }; @@ -379,8 +368,6 @@ public class WebSocketServerTest extends WebSocketTester { super.onOpen(coreSession); callback.succeeded(); - coreSession.demand(); - coreSession.demand(); } @Override @@ -389,6 +376,7 @@ public class WebSocketServerTest extends WebSocketTester LOG.info("onFrame: " + BufferUtil.toDetailString(frame.getPayload())); receivedFrames.offer(frame); receivedCallbacks.offer(callback); + demand(); } }; From c002adbfd7edae9decd6db5d69dfca0064cc2c99 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Wed, 28 Jun 2023 08:50:29 +0200 Subject: [PATCH 47/63] Fix ServletContext.getResourcePaths() (#9974) #9972 Fix ServletContextApi.getResource* path normalization Signed-off-by: Ludovic Orban --- .../ee10/servlet/ServletContextHandler.java | 42 +++++++----- jetty-ee10/jetty-ee10-webapp/pom.xml | 1 + .../org/acme/webapp/GetRealPathsServlet.java | 49 ++++++++++++++ .../acme/webapp/GetResourcePathsServlet.java | 47 ++++++++++++++ .../jetty/ee10/webapp/WebAppContextTest.java | 61 +++++++++++++++++- .../WEB-INF/lib/odd-resource.jar | Bin 0 -> 479 bytes .../webapp-with-resources/WEB-INF/web.xml | 30 +++++++++ 7 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetRealPathsServlet.java create mode 100644 jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetResourcePathsServlet.java create mode 100644 jetty-ee10/jetty-ee10-webapp/src/test/webapp-with-resources/WEB-INF/lib/odd-resource.jar create mode 100644 jetty-ee10/jetty-ee10-webapp/src/test/webapp-with-resources/WEB-INF/web.xml diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java index d7393b17b5f..de675af3fc0 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java @@ -2747,10 +2747,10 @@ public class ServletContextHandler extends ContextHandler @Override public String getRealPath(String path) { - // This is an API call from the application which may pass non-canonical paths. - // Thus, we canonicalize here, to avoid the enforcement of canonical paths in - // ContextHandler.this.getResource(path). - path = URIUtil.canonicalPath(path); + // This is an API call from the application which may pass non-normalized paths. + // Thus, we normalize here, to avoid the enforcement of normalized paths in + // ServletContextHandler.this.getResource(path). + path = URIUtil.normalizePath(path); if (path == null) return null; if (path.length() == 0) @@ -2761,12 +2761,22 @@ public class ServletContextHandler extends ContextHandler try { Resource resource = ServletContextHandler.this.getResource(path); - if (resource != null) + if (resource == null) + return null; + + for (Resource r : resource) { - Path resourcePath = resource.getPath(); - if (resourcePath != null) - return resourcePath.normalize().toString(); + // return first + if (Resources.exists(r)) + { + Path resourcePath = r.getPath(); + if (resourcePath != null) + return resourcePath.normalize().toString(); + } } + + // A Resource was returned, but did not exist + return null; } catch (Exception e) { @@ -2779,10 +2789,10 @@ public class ServletContextHandler extends ContextHandler @Override public URL getResource(String path) throws MalformedURLException { - // This is an API call from the application which may pass non-canonical paths. - // Thus, we canonicalize here, to avoid the enforcement of canonical paths in - // ContextHandler.this.getResource(path). - path = URIUtil.canonicalPath(path); + // This is an API call from the application which may pass non-normalized paths. + // Thus, we normalize here, to avoid the enforcement of normalized paths in + // ServletContextHandler.this.getResource(path). + path = URIUtil.normalizePath(path); if (path == null) return null; @@ -2829,10 +2839,10 @@ public class ServletContextHandler extends ContextHandler @Override public Set getResourcePaths(String path) { - // This is an API call from the application which may pass non-canonical paths. - // Thus, we canonicalize here, to avoid the enforcement of canonical paths in - // ContextHandler.this.getResource(path). - path = URIUtil.canonicalPath(path); + // This is an API call from the application which may pass non-normalized paths. + // Thus, we normalize here, to avoid the enforcement of normalized paths in + // ServletContextHandler.this.getResource(path). + path = URIUtil.normalizePath(path); if (path == null) return null; return ServletContextHandler.this.getResourcePaths(path); diff --git a/jetty-ee10/jetty-ee10-webapp/pom.xml b/jetty-ee10/jetty-ee10-webapp/pom.xml index 8c49ce2a57c..3613ed5a75d 100644 --- a/jetty-ee10/jetty-ee10-webapp/pom.xml +++ b/jetty-ee10/jetty-ee10-webapp/pom.xml @@ -37,6 +37,7 @@ @{argLine} ${jetty.surefire.argLine} + --add-exports org.eclipse.jetty.ee10.webapp/org.acme.webapp=org.eclipse.jetty.ee10.servlet false diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetRealPathsServlet.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetRealPathsServlet.java new file mode 100644 index 00000000000..a99fabdbf90 --- /dev/null +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetRealPathsServlet.java @@ -0,0 +1,49 @@ +// +// ======================================================================== +// 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.acme.webapp; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class GetRealPathsServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + collectResourcePaths("/").stream() + .map(p -> getServletContext().getRealPath(p)) + .forEach(resp.getWriter()::println); + resp.getWriter().flush(); + } + + private Set collectResourcePaths(String path) + { + Set allResourcePaths = new LinkedHashSet<>(); + Set pathsForPath = getServletContext().getResourcePaths(path); + if (pathsForPath != null) + { + for (String resourcePath : pathsForPath) + { + allResourcePaths.add(resourcePath); + allResourcePaths.addAll(collectResourcePaths(resourcePath)); + } + } + return allResourcePaths; + } +} diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetResourcePathsServlet.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetResourcePathsServlet.java new file mode 100644 index 00000000000..2c7aebf9049 --- /dev/null +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/acme/webapp/GetResourcePathsServlet.java @@ -0,0 +1,47 @@ +// +// ======================================================================== +// 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.acme.webapp; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class GetResourcePathsServlet extends HttpServlet +{ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + collectResourcePaths("/").forEach(resp.getWriter()::println); + resp.getWriter().flush(); + } + + private Set collectResourcePaths(String path) + { + Set allResourcePaths = new LinkedHashSet<>(); + Set pathsForPath = getServletContext().getResourcePaths(path); + if (pathsForPath != null) + { + for (String resourcePath : pathsForPath) + { + allResourcePaths.add(resourcePath); + allResourcePaths.addAll(collectResourcePaths(resourcePath)); + } + } + return allResourcePaths; + } +} diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java index 1fd5c44b5cd..f0e8b580bd5 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java @@ -67,8 +67,10 @@ import org.slf4j.LoggerFactory; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -230,7 +232,7 @@ public class WebAppContextTest } @Test - public void testRealPathDoesNotExist() throws Exception + public void testRealPath() throws Exception { Server server = newServer(); WebAppContext context = new WebAppContext(".", "/"); @@ -238,6 +240,7 @@ public class WebAppContextTest server.start(); ServletContext ctx = context.getServletContext(); + assertNotNull(ctx.getRealPath("/")); assertNull(ctx.getRealPath("/doesnotexist")); assertNull(ctx.getRealPath("/doesnotexist/")); } @@ -536,6 +539,62 @@ public class WebAppContextTest assertThat(servletContext.getResourcePaths("/WEB-INF"), containsInAnyOrder("/WEB-INF/zero.xml", "/WEB-INF/one.xml")); } + @Test + public void testGetResourcePaths() throws Exception + { + Server server = newServer(); + LocalConnector connector = new LocalConnector(server); + server.addConnector(connector); + + WebAppContext context = new WebAppContext(MavenTestingUtils.getBasePath().resolve("src/test/webapp-with-resources").toString(), "/"); + server.setHandler(context); + server.start(); + + ServletContext servletContext = context.getServletContext(); + + List resourcePaths = List.copyOf(servletContext.getResourcePaths("/")); + assertThat(resourcePaths.size(), is(2)); + assertThat(resourcePaths.get(0), is("/WEB-INF")); + assertThat(resourcePaths.get(1), is("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt")); + + String realPath = servletContext.getRealPath("/"); + assertThat(realPath, notNullValue()); + assertThat(servletContext.getRealPath(resourcePaths.get(0)), endsWith("/WEB-INF")); + // TODO the following assertion fails because of a bug in the JDK (see #9978) + //assertThat(servletContext.getRealPath(resourcePaths.get(1)), endsWith("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt")); + + assertThat(servletContext.getResource("/WEB-INF"), notNullValue()); + // TODO the following assertion fails because of a bug in the JDK (see #9978) + //assertThat(servletContext.getResource("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt"), notNullValue()); + + HttpTester.Response response1 = HttpTester.parseResponse(connector.getResponse(""" + GET /resource HTTP/1.1\r + Host: local\r + Connection: close\r + \r + """)); + + assertThat(response1.getStatus(), is(HttpStatus.OK_200)); + assertThat(response1.getContent(), containsString("/WEB-INF")); + assertThat(response1.getContent(), containsString("/WEB-INF/lib")); + assertThat(response1.getContent(), containsString("/WEB-INF/lib/odd-resource.jar")); + assertThat(response1.getContent(), containsString("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt")); + + HttpTester.Response response2 = HttpTester.parseResponse(connector.getResponse(""" + GET /real HTTP/1.1\r + Host: local\r + Connection: close\r + \r + """)); + + assertThat(response2.getStatus(), is(HttpStatus.OK_200)); + assertThat(response2.getContent(), containsString("/WEB-INF")); + assertThat(response2.getContent(), containsString("/WEB-INF/lib")); + assertThat(response2.getContent(), containsString("/WEB-INF/lib/odd-resource.jar")); + // TODO the following assertion fails because of a bug in the JDK (see #9978) + //assertThat(response2.getContent(), containsString("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt")); + } + public static Stream extraClasspathGlob() { List references = new ArrayList<>(); diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/webapp-with-resources/WEB-INF/lib/odd-resource.jar b/jetty-ee10/jetty-ee10-webapp/src/test/webapp-with-resources/WEB-INF/lib/odd-resource.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c78785fd6a31202febce862f09227dfb6557e1a GIT binary patch literal 479 zcmWIWW@Zs#0D%e9ZiRsvP=XUk`?`iW>U#RQ=?CCcCXB4CD784hv?w{X7)e7L(AT4qkF0+7zjFHuO%Ehwqv3h-uRvS-HaGoT~D;IAWyLh>hWZ6Ke* vz+XpoAQP?)<{*&25e8uMIn019jS<)lAU7NWyjj^mhBE` + + + + GetResourcePathsServlet + org.acme.webapp.GetResourcePathsServlet + 1 + + + GetRealPathsServlet + org.acme.webapp.GetRealPathsServlet + 1 + + + + GetResourcePathsServlet + /resource/* + + + GetRealPathsServlet + /real/* + + + + From 617821445ad7a367c2b17e278a169a1ec9e94ebd Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 28 Jun 2023 12:07:11 +0200 Subject: [PATCH 48/63] Fix #9960 Improve Request methods, including NPE protection. (#9971) Fix #9960 with NPE protection for bad requests. --- .../main/java/org/eclipse/jetty/server/Request.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index ae6a849c1b4..69ceca7628c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -122,7 +122,7 @@ public interface Request extends Attributes, Content.Source /** * an ID unique within the lifetime scope of the {@link ConnectionMetaData#getId()}). - * This may be a protocol ID (eg HTTP/2 stream ID) or it may be unrelated to the protocol. + * This may be a protocol ID (e.g. HTTP/2 stream ID) or it may be unrelated to the protocol. * * @see HttpStream#getId() */ @@ -243,7 +243,7 @@ public interface Request extends Attributes, Content.Source /** * Consume any available content. This bypasses any request wrappers to process the content in * {@link Request#read()} and reads directly from the {@link HttpStream}. This reads until - * there is no content currently available or it reaches EOF. + * there is no content currently available, or it reaches EOF. * The {@link HttpConfiguration#setMaxUnconsumedRequestContentReads(int)} configuration can be used * to configure how many reads will be attempted by this method. * @return true if the content was fully consumed. @@ -360,7 +360,7 @@ public interface Request extends Attributes, Content.Source : address.getHostAddress(); return HostPort.normalizeHost(result); } - return local.toString(); + return local == null ? null : local.toString(); } static int getLocalPort(Request request) @@ -389,7 +389,7 @@ public interface Request extends Attributes, Content.Source : address.getHostAddress(); return HostPort.normalizeHost(result); } - return remote.toString(); + return remote == null ? null : remote.toString(); } static int getRemotePort(Request request) @@ -419,7 +419,7 @@ public interface Request extends Attributes, Content.Source if (local instanceof InetSocketAddress) return HostPort.normalizeHost(((InetSocketAddress)local).getHostString()); - return local.toString(); + return local == null ? null : local.toString(); } static int getServerPort(Request request) @@ -607,7 +607,7 @@ public interface Request extends Attributes, Content.Source * @param request the HTTP request to handle * @param response the HTTP response to handle * @param callback the callback to complete when the handling is complete - * @return True if an only if the request will be handled, a response generated and the callback eventually called. + * @return True if and only if the request will be handled, a response generated and the callback eventually called. * This may occur within the scope of the call to this method, or asynchronously some time later. If false * is returned, then this method must not generate a response, nor complete the callback. * @throws Exception if there is a failure during the handling. Catchers cannot assume that the callback will be From daa7167834741d29152baaf310c91f7e9e21cd0e Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 28 Jun 2023 14:22:13 +0200 Subject: [PATCH 49/63] Implement quality lists for Locales (#9983) Implement quality lists for Locales that orders known locales before unknown. --- .../org/eclipse/jetty/http/MimeTypes.java | 8 ++++ .../org/eclipse/jetty/server/Request.java | 34 +++++++++-------- .../org/eclipse/jetty/server/RequestTest.java | 38 +++++++++++++++++++ .../jetty/server/handler/DumpHandler.java | 2 + 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 8cc493092f5..ad7d4991c0e 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -18,12 +18,14 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Properties; +import java.util.Set; import org.eclipse.jetty.util.FileID; import org.eclipse.jetty.util.Index; @@ -37,6 +39,12 @@ import org.slf4j.LoggerFactory; public class MimeTypes { static final Logger LOG = LoggerFactory.getLogger(MimeTypes.class); + private static final Set KNOWN_LOCALES = Set.copyOf(Arrays.asList(Locale.getAvailableLocales())); + + public static boolean isKnownLocale(Locale locale) + { + return KNOWN_LOCALES.contains(locale); + } /** Enumeration of predefined MimeTypes. This is not exhaustive */ public enum Type diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 69ceca7628c..e84dad2ca79 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -20,22 +20,22 @@ import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.Principal; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collectors; import org.eclipse.jetty.http.CookieCache; import org.eclipse.jetty.http.HttpCookie; -import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.Trailers; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.internal.HttpChannelState; @@ -119,6 +119,7 @@ public interface Request extends Attributes, Content.Source { String CACHE_ATTRIBUTE = Request.class.getCanonicalName() + ".CookieCache"; String COOKIE_ATTRIBUTE = Request.class.getCanonicalName() + ".Cookies"; + List DEFAULT_LOCALES = List.of(Locale.getDefault()); /** * an ID unique within the lifetime scope of the {@link ConnectionMetaData#getId()}). @@ -452,26 +453,27 @@ public interface Request extends Attributes, Content.Source { HttpFields fields = request.getHeaders(); if (fields == null) - return List.of(Locale.getDefault()); + return DEFAULT_LOCALES; List acceptable = fields.getQualityCSV(HttpHeader.ACCEPT_LANGUAGE); - // handle no locale - if (acceptable.isEmpty()) - return List.of(Locale.getDefault()); - - return acceptable.stream().map(language -> + // return sorted list of locals, with known locales in quality order before unknown locales in quality order + return switch (acceptable.size()) { - language = HttpField.stripParameters(language); - String country = ""; - int dash = language.indexOf('-'); - if (dash > -1) + case 0 -> DEFAULT_LOCALES; + case 1 -> List.of(Locale.forLanguageTag(acceptable.get(0))); + default -> { - country = language.substring(dash + 1).trim(); - language = language.substring(0, dash).trim(); + List locales = acceptable.stream().map(Locale::forLanguageTag).toList(); + List known = locales.stream().filter(MimeTypes::isKnownLocale).toList(); + if (known.size() == locales.size()) + yield locales; // All locales are known + List unknown = locales.stream().filter(l -> !MimeTypes.isKnownLocale(l)).toList(); + locales = new ArrayList<>(known); + locales.addAll(unknown); + yield locales; // List of known locales before unknown locales } - return new Locale(language, country); - }).collect(Collectors.toList()); + }; } // TODO: consider inline and remove. diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 08d9f453402..d81bab9c4e7 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -17,7 +17,9 @@ import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; @@ -30,6 +32,9 @@ import org.eclipse.jetty.util.component.LifeCycle; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -372,6 +377,39 @@ public class RequestTest assertThat(response.getStatus(), is(HttpStatus.OK_200)); } + public static Stream localeTests() + { + return Stream.of( + Arguments.of(null, List.of(Locale.getDefault().toLanguageTag()).toString()), + Arguments.of("zz", "[zz]"), + Arguments.of("en", "[en]"), + Arguments.of("en-gb", List.of(Locale.UK.toLanguageTag()).toString()), + Arguments.of("en-us", List.of(Locale.US.toLanguageTag()).toString()), + Arguments.of("EN-US", List.of(Locale.US.toLanguageTag()).toString()), + Arguments.of("en-us,en-gb", List.of(Locale.US.toLanguageTag(), Locale.UK.toLanguageTag()).toString()), + Arguments.of("en-us;q=0.5,fr;q=0.0,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag()).toString()), + Arguments.of("en-us;q=0.5,zz-yy;q=0.7,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag(), "zz-YY").toString()) + ); + } + + @ParameterizedTest + @MethodSource("localeTests") + public void testAcceptableLocales(String acceptLanguage, String expectedLocales) throws Exception + { + acceptLanguage = acceptLanguage == null ? "" : (HttpHeader.ACCEPT_LANGUAGE.asString() + ": " + acceptLanguage + "\n"); + String rawRequest = """ + GET / HTTP/1.1 + Host: tester + Connection: close + %s + """.formatted(acceptLanguage); + + HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest)); + assertNotNull(response); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContent(), containsString("locales=" + expectedLocales)); + } + private static void checkCookieResult(String containedCookie, String[] notContainedCookies, String response) { assertNotNull(containedCookie); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java index 783b4e76304..8e403711355 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java @@ -17,6 +17,7 @@ import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.util.Locale; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; @@ -151,6 +152,7 @@ public class DumpHandler extends Handler.Abstract writer.write("
    httpURI.path=" + httpURI.getPath() + "

    \n"); writer.write("
    httpURI.query=" + httpURI.getQuery() + "

    \n"); writer.write("
    httpURI.pathQuery=" + httpURI.getPathQuery() + "

    \n"); + writer.write("
    locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "

    \n"); writer.write("
    pathInContext=" + Request.getPathInContext(request) + "

    \n"); writer.write("
    contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "

    \n"); writer.write("
    servername=" + Request.getServerName(request) + "

    \n"); From cc3bac27a551ffeb4893bb13f2718e9b6cfa60b7 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Wed, 28 Jun 2023 18:36:00 +0200 Subject: [PATCH 50/63] #9984 fix URLResourceFactory isDirectory and newReadableByteChannel (#9985) * #9984 fix URLResourceFactory isDirectory and newReadableByteChannel Signed-off-by: Ludovic Orban --- .../util/resource/URLResourceFactory.java | 8 +- .../util/resource/UrlResourceFactoryTest.java | 118 ++++++++++++++---- 2 files changed, 97 insertions(+), 29 deletions(-) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java index 39bacbe4100..243a3ad8fa0 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java @@ -19,6 +19,7 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLConnection; +import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.file.Path; import java.time.Instant; @@ -109,7 +110,7 @@ public class URLResourceFactory implements ResourceFactory @Override public boolean isDirectory() { - return uri.getPath().endsWith("/"); + return uri.getSchemeSpecificPart().endsWith("/"); } @Override @@ -200,10 +201,9 @@ public class URLResourceFactory implements ResourceFactory } @Override - public ReadableByteChannel newReadableByteChannel() + public ReadableByteChannel newReadableByteChannel() throws IOException { - // not really possible with the URL interface - return null; + return Channels.newChannel(newInputStream()); } @Override diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java index fe783a26816..47fd703f08e 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java @@ -16,9 +16,15 @@ package org.eclipse.jetty.util.resource; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Instant; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -38,35 +44,97 @@ public class UrlResourceFactoryTest public void testHttps() throws IOException { ResourceFactory.registerResourceFactory("https", new URLResourceFactory()); - Resource resource = ResourceFactory.root().newResource(URI.create("https://webtide.com/")); - assertThat(resource, notNullValue()); - assertTrue(resource.exists()); - - try (InputStream in = resource.newInputStream()) + try { - String result = IO.toString(in, StandardCharsets.UTF_8); - assertThat(result, containsString("webtide.com")); + Resource resource = ResourceFactory.root().newResource(URI.create("https://webtide.com/")); + assertThat(resource, notNullValue()); + assertTrue(resource.exists()); + + try (InputStream in = resource.newInputStream()) + { + String result = IO.toString(in, StandardCharsets.UTF_8); + assertThat(result, containsString("webtide.com")); + } + + assertThat(resource.lastModified().toEpochMilli(), not(Instant.EPOCH)); + assertThat(resource.length(), not(-1)); + assertTrue(resource.isDirectory()); + assertThat(resource.getFileName(), is("")); + + Resource blogs = resource.resolve("blogs/"); + assertThat(blogs, notNullValue()); + assertTrue(blogs.exists()); + assertThat(blogs.lastModified().toEpochMilli(), not(Instant.EPOCH)); + assertThat(blogs.length(), not(-1)); + assertTrue(blogs.isDirectory()); + assertThat(blogs.getFileName(), is("")); + + Resource favicon = resource.resolve("favicon.ico"); + assertThat(favicon, notNullValue()); + assertTrue(favicon.exists()); + assertThat(favicon.lastModified().toEpochMilli(), not(Instant.EPOCH)); + assertThat(favicon.length(), not(-1)); + assertFalse(favicon.isDirectory()); + assertThat(favicon.getFileName(), is("favicon.ico")); } + finally + { + ResourceFactory.unregisterResourceFactory("https"); + } + } - assertThat(resource.lastModified().toEpochMilli(), not(Instant.EPOCH)); - assertThat(resource.length(), not(-1)); - assertTrue(resource.isDirectory()); - assertThat(resource.getFileName(), is("")); + @Test + public void testFileUrl() throws Exception + { + Path path = MavenTestingUtils.getTestResourcePath("example.jar"); + int fileSize = (int)Files.size(path); + URL fileUrl = new URL("file:" + path.toAbsolutePath()); + URLResourceFactory urlResourceFactory = new URLResourceFactory(); + Resource resource = urlResourceFactory.newResource(fileUrl); - Resource blogs = resource.resolve("blogs/"); - assertThat(blogs, notNullValue()); - assertTrue(blogs.exists()); - assertThat(blogs.lastModified().toEpochMilli(), not(Instant.EPOCH)); - assertThat(blogs.length(), not(-1)); - assertTrue(blogs.isDirectory()); - assertThat(blogs.getFileName(), is("")); + assertThat(resource.isDirectory(), is(false)); - Resource favicon = resource.resolve("favicon.ico"); - assertThat(favicon, notNullValue()); - assertTrue(favicon.exists()); - assertThat(favicon.lastModified().toEpochMilli(), not(Instant.EPOCH)); - assertThat(favicon.length(), not(-1)); - assertFalse(favicon.isDirectory()); - assertThat(favicon.getFileName(), is("favicon.ico")); + try (ReadableByteChannel channel = resource.newReadableByteChannel()) + { + ByteBuffer buffer = ByteBuffer.allocate(fileSize); + int read = channel.read(buffer); + assertThat(read, is(fileSize)); + } + } + + @Test + public void testJarFileUrl() throws Exception + { + Path path = MavenTestingUtils.getTestResourcePath("example.jar"); + URL jarFileUrl = new URL("jar:file:" + path.toAbsolutePath() + "!/WEB-INF/web.xml"); + int fileSize = (int)fileSize(jarFileUrl); + URLResourceFactory urlResourceFactory = new URLResourceFactory(); + Resource resource = urlResourceFactory.newResource(jarFileUrl); + + assertThat(resource.isDirectory(), is(false)); + + try (ReadableByteChannel channel = resource.newReadableByteChannel()) + { + ByteBuffer buffer = ByteBuffer.allocate(fileSize); + int read = channel.read(buffer); + assertThat(read, is(fileSize)); + } + } + + private static long fileSize(URL url) throws IOException + { + try (InputStream is = url.openStream()) + { + long totalRead = 0; + byte[] buffer = new byte[512]; + while (true) + { + int read = is.read(buffer); + if (read == -1) + break; + totalRead += read; + } + return totalRead; + } } } From 20869acff4f13c705f483abb2efb0c8529574d65 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Wed, 28 Jun 2023 18:51:35 +0200 Subject: [PATCH 51/63] Add plexus-xml to test-distribution --- tests/test-distribution/pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test-distribution/pom.xml b/tests/test-distribution/pom.xml index 3e21f60789a..662aeca4d56 100644 --- a/tests/test-distribution/pom.xml +++ b/tests/test-distribution/pom.xml @@ -17,6 +17,17 @@ + + org.codehaus.plexus + plexus-xml + test + + + javax.annotation + javax.annotation-api + + + org.eclipse.jetty.tests jetty-home-tester From 0863b555dd3cb0a8bb3e4c0f819220dab3949e95 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 28 Jun 2023 14:04:21 -0500 Subject: [PATCH 52/63] Fix Path to URL conversion for windows users (#9989) * Fix Path to URL conversion for windows users --- .../eclipse/jetty/util/resource/UrlResourceFactoryTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java index 47fd703f08e..0fed754b86c 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java @@ -88,7 +88,7 @@ public class UrlResourceFactoryTest { Path path = MavenTestingUtils.getTestResourcePath("example.jar"); int fileSize = (int)Files.size(path); - URL fileUrl = new URL("file:" + path.toAbsolutePath()); + URL fileUrl = path.toUri().toURL(); URLResourceFactory urlResourceFactory = new URLResourceFactory(); Resource resource = urlResourceFactory.newResource(fileUrl); @@ -106,7 +106,7 @@ public class UrlResourceFactoryTest public void testJarFileUrl() throws Exception { Path path = MavenTestingUtils.getTestResourcePath("example.jar"); - URL jarFileUrl = new URL("jar:file:" + path.toAbsolutePath() + "!/WEB-INF/web.xml"); + URL jarFileUrl = new URL("jar:" + path.toUri().toASCIIString() + "!/WEB-INF/web.xml"); int fileSize = (int)fileSize(jarFileUrl); URLResourceFactory urlResourceFactory = new URLResourceFactory(); Resource resource = urlResourceFactory.newResource(jarFileUrl); From 48bfc70b2d0d56255d3d52f989669b137b134fb2 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 29 Jun 2023 08:04:18 +1000 Subject: [PATCH 53/63] use enum for DemandState in WebSocketConnection Signed-off-by: Lachlan Roberts --- .../websocket/core/WebSocketConnection.java | 39 +++++++++++++------ .../websocket/core/util/DemandingFlusher.java | 2 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java index c429bcde0a8..ca72e5533d0 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java @@ -54,6 +54,13 @@ public class WebSocketConnection extends AbstractConnection implements Connectio */ private static final int MIN_BUFFER_SIZE = Generator.MAX_HEADER_LENGTH; + private enum DemandState + { + DEMANDING, + NOT_DEMANDING, + CANCELLED + } + private final AutoLock lock = new AutoLock(); private final ByteBufferPool byteBufferPool; private final Generator generator; @@ -61,7 +68,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio private final WebSocketCoreSession coreSession; private final Flusher flusher; private final Random random; - private Boolean demand = false; + private DemandState demand = DemandState.NOT_DEMANDING; private boolean fillingAndParsing; private final LongAdder messagesIn = new LongAdder(); private final LongAdder bytesIn = new LongAdder(); @@ -353,9 +360,9 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (demand != null) { - if (demand) + if (demand == DemandState.DEMANDING) throw new ReadPendingException(); - demand = true; + demand = DemandState.DEMANDING; } if (!fillingAndParsing) @@ -381,13 +388,23 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (!fillingAndParsing) throw new IllegalStateException(); - if (demand != Boolean.FALSE) // If demand was canceled, this creates synthetic demand in order to read until EOF. - return true; - fillingAndParsing = false; - if (!networkBuffer.hasRemaining()) - releaseNetworkBuffer(); - return false; + switch (demand) + { + case NOT_DEMANDING -> + { + fillingAndParsing = false; + if (!networkBuffer.hasRemaining()) + releaseNetworkBuffer(); + return false; + } + case DEMANDING, CANCELLED -> + { + // If demand was canceled, this creates synthetic demand in order to read until EOF. + return true; + } + default -> throw new IllegalStateException(demand.name()); + } } } @@ -398,13 +415,13 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (LOG.isDebugEnabled()) LOG.debug("meetDemand d={} fp={} {} {}", demand, fillingAndParsing, networkBuffer, this); - if (demand == Boolean.FALSE) + if (demand == DemandState.NOT_DEMANDING) throw new IllegalStateException(); if (!fillingAndParsing) throw new IllegalStateException(); if (demand != null) - demand = false; + demand = DemandState.NOT_DEMANDING; return true; } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java index 85eac40fd2b..9fe294d15f5 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/DemandingFlusher.java @@ -142,7 +142,7 @@ public abstract class DemandingFlusher extends IteratingCallback implements Dema public void emitFrame(Frame frame, Callback callback) { if (!_demand.compareAndSet(true, false)) - throw new IllegalStateException("Demand Already Fulfilled"); + throw new IllegalStateException("Demand already fulfilled"); _emitFrame.onFrame(frame, callback); } From a10b98e95bf5284ee7ff269cb7fc4154fea594fa Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Thu, 29 Jun 2023 09:29:58 +0200 Subject: [PATCH 54/63] add JDK bug ID Signed-off-by: Ludovic Orban --- .../jetty/util/resource/MountedPathResourceTest.java | 2 +- .../org/eclipse/jetty/ee10/webapp/WebAppContextTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java index b2e09c2e0b9..b000308813a 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MountedPathResourceTest.java @@ -323,7 +323,7 @@ public class MountedPathResourceTest * Test resolving a resource that has a backslash */ @Test - @Disabled("Test disabled due to a JDK bug") + @Disabled("Test disabled due to JDK bug JDK-8311079") public void testJarFileResourceAccessBackSlash() throws Exception { Path testJar = MavenTestingUtils.getTestResourcePathFile("jar-file-resource.jar"); diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java index f0e8b580bd5..14238ec3995 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java @@ -560,11 +560,11 @@ public class WebAppContextTest String realPath = servletContext.getRealPath("/"); assertThat(realPath, notNullValue()); assertThat(servletContext.getRealPath(resourcePaths.get(0)), endsWith("/WEB-INF")); - // TODO the following assertion fails because of a bug in the JDK (see #9978) + // TODO the following assertion fails because of a bug in the JDK (see JDK-8311079 and MountedPathResourceTest.testJarFileResourceAccessBackSlash()) //assertThat(servletContext.getRealPath(resourcePaths.get(1)), endsWith("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt")); assertThat(servletContext.getResource("/WEB-INF"), notNullValue()); - // TODO the following assertion fails because of a bug in the JDK (see #9978) + // TODO the following assertion fails because of a bug in the JDK (see JDK-8311079 and MountedPathResourceTest.testJarFileResourceAccessBackSlash()) //assertThat(servletContext.getResource("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt"), notNullValue()); HttpTester.Response response1 = HttpTester.parseResponse(connector.getResponse(""" @@ -591,7 +591,7 @@ public class WebAppContextTest assertThat(response2.getContent(), containsString("/WEB-INF")); assertThat(response2.getContent(), containsString("/WEB-INF/lib")); assertThat(response2.getContent(), containsString("/WEB-INF/lib/odd-resource.jar")); - // TODO the following assertion fails because of a bug in the JDK (see #9978) + // TODO the following assertion fails because of a bug in the JDK (see JDK-8311079 and MountedPathResourceTest.testJarFileResourceAccessBackSlash()) //assertThat(response2.getContent(), containsString("/nested-reserved-!#\\\\$%&()*+,:=?@[]-meta-inf-resource.txt")); } From 867fe46e7531d492ad5f41bf9655918f08a72c28 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 29 Jun 2023 20:31:43 +1000 Subject: [PATCH 55/63] fix DemandState in WebSocketConnection Signed-off-by: Lachlan Roberts --- .../eclipse/jetty/websocket/core/WebSocketConnection.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java index ca72e5533d0..fa6d2524a6b 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java @@ -358,7 +358,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (LOG.isDebugEnabled()) LOG.debug("demand {} d={} fp={} {}", demand, fillingAndParsing, networkBuffer, this); - if (demand != null) + if (demand != DemandState.CANCELLED) { if (demand == DemandState.DEMANDING) throw new ReadPendingException(); @@ -420,7 +420,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (!fillingAndParsing) throw new IllegalStateException(); - if (demand != null) + if (demand != DemandState.CANCELLED) demand = DemandState.NOT_DEMANDING; return true; } @@ -432,7 +432,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio { if (LOG.isDebugEnabled()) LOG.debug("cancelDemand d={} fp={} {} {}", demand, fillingAndParsing, networkBuffer, this); - demand = null; + demand = DemandState.CANCELLED; } } From 7b80b0bbcbcaf5605efe2655630e49aa3483144a Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Thu, 29 Jun 2023 12:56:36 +0200 Subject: [PATCH 56/63] Update plexus-utils to 4.0.0 (#9958) * Update plexus-utils to 4.0.0 --------- Co-authored-by: Olivier Lamy --- jetty-maven-plugin/pom.xml | 4 ++++ pom.xml | 8 +++++++- tests/test-distribution/pom.xml | 11 +++++++++++ tests/test-websocket-autobahn/pom.xml | 13 ++++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/jetty-maven-plugin/pom.xml b/jetty-maven-plugin/pom.xml index f28ec134c85..6b0bac97ce3 100644 --- a/jetty-maven-plugin/pom.xml +++ b/jetty-maven-plugin/pom.xml @@ -160,6 +160,10 @@ org.codehaus.plexus plexus-utils + + org.codehaus.plexus + plexus-xml + org.apache.maven.plugin-tools maven-plugin-tools-api diff --git a/pom.xml b/pom.xml index 00afeede81f..e14ea7ee15d 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,8 @@ 1.2.0 1.2.0 2.1.1 - 3.5.1 + 4.0.0 + 4.0.0 2.0.5 2.1.1.RELEASE 1.2.5 @@ -1256,6 +1257,11 @@ plexus-utils ${plexus-utils.version} + + org.codehaus.plexus + plexus-xml + ${plexus-xml.version} + org.eclipse.jetty apache-jsp diff --git a/tests/test-distribution/pom.xml b/tests/test-distribution/pom.xml index 3e21f60789a..662aeca4d56 100644 --- a/tests/test-distribution/pom.xml +++ b/tests/test-distribution/pom.xml @@ -17,6 +17,17 @@ + + org.codehaus.plexus + plexus-xml + test + + + javax.annotation + javax.annotation-api + + + org.eclipse.jetty.tests jetty-home-tester diff --git a/tests/test-websocket-autobahn/pom.xml b/tests/test-websocket-autobahn/pom.xml index 7505b770425..66f8ccfa565 100644 --- a/tests/test-websocket-autobahn/pom.xml +++ b/tests/test-websocket-autobahn/pom.xml @@ -60,6 +60,17 @@ plexus-utils test + + org.codehaus.plexus + plexus-xml + test + + + javax.annotation + javax.annotation-api + + + com.googlecode.json-simple json-simple @@ -131,4 +142,4 @@ - \ No newline at end of file + From caf421e8275ef9ddd819dcdbb864c74e839b6563 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Thu, 29 Jun 2023 10:53:12 +0200 Subject: [PATCH 57/63] remove unnecessary FileBufferedResponseHandler Signed-off-by: Ludovic Orban --- .../handler/FileBufferedResponseHandler.java | 226 ------ .../FileBufferedResponseHandlerTest.java | 645 ------------------ 2 files changed, 871 deletions(-) delete mode 100644 jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java delete mode 100644 jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandlerTest.java diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java deleted file mode 100644 index 49d98efb878..00000000000 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java +++ /dev/null @@ -1,226 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.server.handler; - -import java.io.File; -import java.nio.file.Path; -import java.util.Objects; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - *

    - * A Handler that can apply a - * mechanism to buffer the entire response content until the output is closed. - * This allows the commit to be delayed until the response is complete and thus - * headers and response status can be changed while writing the body. - *

    - *

    - * Note that the decision to buffer is influenced by the headers and status at the - * first write, and thus subsequent changes to those headers will not influence the - * decision to buffer or not. - *

    - *

    - * Note also that there are no memory limits to the size of the buffer, thus - * this handler can represent an unbounded memory commitment if the content - * generated can also be unbounded. - *

    - */ -public class FileBufferedResponseHandler extends BufferedResponseHandler -{ - private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandler.class); - - private Path _tempDir = new File(System.getProperty("java.io.tmpdir")).toPath(); - - public Path getTempDir() - { - return _tempDir; - } - - public void setTempDir(Path tempDir) - { - _tempDir = Objects.requireNonNull(tempDir); - } - - // TODO - /* - @Override - protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor) - { - return new FileBufferedInterceptor(httpChannel, interceptor); - } - - class FileBufferedInterceptor implements BufferedResponseHandler.BufferedInterceptor - { - private static final int MAX_MAPPED_BUFFER_SIZE = Integer.MAX_VALUE / 2; - - private final Interceptor _next; - private final HttpChannel _channel; - private Boolean _aggregating; - private Path _filePath; - private OutputStream _fileOutputStream; - - public FileBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor) - { - _next = interceptor; - _channel = httpChannel; - } - - @Override - public Interceptor getNextInterceptor() - { - return _next; - } - - @Override - public void resetBuffer() - { - dispose(); - BufferedInterceptor.super.resetBuffer(); - } - - protected void dispose() - { - IO.close(_fileOutputStream); - _fileOutputStream = null; - _aggregating = null; - - if (_filePath != null) - { - try - { - Files.delete(_filePath); - } - catch (Throwable t) - { - if (LOG.isDebugEnabled()) - LOG.debug("Could not immediately delete file (delaying to jvm exit) {}", _filePath, t); - _filePath.toFile().deleteOnExit(); - } - _filePath = null; - } - } - - @Override - public void write(ByteBuffer content, boolean last, Callback callback) - { - if (LOG.isDebugEnabled()) - LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content)); - - // If we are not committed, must decide if we should aggregate or not. - if (_aggregating == null) - _aggregating = shouldBuffer(_channel, last); - - // If we are not aggregating, then handle normally. - if (!_aggregating) - { - getNextInterceptor().write(content, last, callback); - return; - } - - if (LOG.isDebugEnabled()) - LOG.debug("{} aggregating", this); - - try - { - if (BufferUtil.hasContent(content)) - aggregate(content); - } - catch (Throwable t) - { - dispose(); - callback.failed(t); - return; - } - - if (last) - commit(callback); - else - callback.succeeded(); - } - - private void aggregate(ByteBuffer content) throws IOException - { - if (_fileOutputStream == null) - { - // Create a new OutputStream to a file. - _filePath = Files.createTempFile(_tempDir, "BufferedResponse", ""); - _fileOutputStream = Files.newOutputStream(_filePath, StandardOpenOption.WRITE); - } - - BufferUtil.writeTo(content, _fileOutputStream); - } - - private void commit(Callback callback) - { - if (_fileOutputStream == null) - { - // We have no content to write, signal next interceptor that we are finished. - getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback); - return; - } - - try - { - _fileOutputStream.close(); - _fileOutputStream = null; - } - catch (Throwable t) - { - dispose(); - callback.failed(t); - return; - } - - // Create an iterating callback to do the writing - IteratingCallback icb = new IteratingCallback() - { - private final long fileLength = _filePath.toFile().length(); - private long _pos = 0; - private boolean _last = false; - - @Override - protected Action process() throws Exception - { - if (_last) - return Action.SUCCEEDED; - - long len = Math.min(MAX_MAPPED_BUFFER_SIZE, fileLength - _pos); - _last = (_pos + len == fileLength); - ByteBuffer buffer = BufferUtil.toMappedBuffer(_filePath, _pos, len); - getNextInterceptor().write(buffer, _last, this); - _pos += len; - return Action.SCHEDULED; - } - - @Override - protected void onCompleteSuccess() - { - dispose(); - callback.succeeded(); - } - - @Override - protected void onCompleteFailure(Throwable cause) - { - dispose(); - callback.failed(cause); - } - }; - icb.iterate(); - } - } - */ -} diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandlerTest.java deleted file mode 100644 index e361cc218d3..00000000000 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandlerTest.java +++ /dev/null @@ -1,645 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.server.handler; - -import java.io.File; -import java.nio.file.Path; -import java.util.Random; -import java.util.concurrent.CountDownLatch; - -import org.eclipse.jetty.server.LocalConnector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.extension.ExtendWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@Disabled // TODO -@ExtendWith(WorkDirExtension.class) -public class FileBufferedResponseHandlerTest -{ - private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandlerTest.class); - - public WorkDir _workDir; - - private final CountDownLatch _disposeLatch = new CountDownLatch(1); - private Server _server; - private LocalConnector _localConnector; - private ServerConnector _serverConnector; - private Path _testDir; - private FileBufferedResponseHandler _bufferedHandler; - - /* TODO - @BeforeEach - public void before() throws Exception - { - _testDir = _workDir.getEmptyPathDir(); - - _server = new Server(); - HttpConfiguration config = new HttpConfiguration(); - config.setOutputBufferSize(1024); - config.setOutputAggregationSize(256); - - _localConnector = new LocalConnector(_server, new HttpConnectionFactory(config)); - _localConnector.setIdleTimeout(Duration.ofMinutes(1).toMillis()); - _server.addConnector(_localConnector); - _serverConnector = new ServerConnector(_server, new HttpConnectionFactory(config)); - _server.addConnector(_serverConnector); - - _bufferedHandler = new FileBufferedResponseHandler() - { - @Override - protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, HttpOutput.Interceptor interceptor) - { - return new FileBufferedInterceptor(httpChannel, interceptor) - { - @Override - protected void dispose() - { - super.dispose(); - _disposeLatch.countDown(); - } - }; - } - }; - _bufferedHandler.setTempDir(_testDir); - _bufferedHandler.getPathIncludeExclude().include("/include/*"); - _bufferedHandler.getPathIncludeExclude().exclude("*.exclude"); - _bufferedHandler.getMimeIncludeExclude().exclude("text/excluded"); - _server.setHandler(_bufferedHandler); - } - - @AfterEach - public void after() throws Exception - { - _server.stop(); - } - - @Test - public void testPathNotIncluded() throws Exception - { - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(10); - PrintWriter writer = response.getWriter(); - writer.println("a string larger than the buffer size"); - writer.println("Committed: " + response.isCommitted()); - writer.println("NumFiles: " + getNumFiles()); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The response was committed after the first write and we never created a file to buffer the response into. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, containsString("Committed: true")); - assertThat(responseContent, containsString("NumFiles: 0")); - assertThat(getNumFiles(), is(0)); - } - - @Test - public void testIncludedByPath() throws Exception - { - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(10); - PrintWriter writer = response.getWriter(); - writer.println("a string larger than the buffer size"); - writer.println("Committed: " + response.isCommitted()); - writer.println("NumFiles: " + getNumFiles()); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The response was not committed after the first write and a file was created to buffer the response. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, containsString("Committed: false")); - assertThat(responseContent, containsString("NumFiles: 1")); - - // Unable to verify file deletion on windows, as immediate delete not possible. - // only after a GC has occurred. - if (!OS.WINDOWS.isCurrentOs()) - { - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - } - - @Test - public void testExcludedByPath() throws Exception - { - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(10); - PrintWriter writer = response.getWriter(); - writer.println("a string larger than the buffer size"); - writer.println("Committed: " + response.isCommitted()); - writer.println("NumFiles: " + getNumFiles()); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The response was committed after the first write and we never created a file to buffer the response into. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, containsString("Committed: true")); - assertThat(responseContent, containsString("NumFiles: 0")); - assertThat(getNumFiles(), is(0)); - } - - @Test - public void testExcludedByMime() throws Exception - { - String excludedMimeType = "text/excluded"; - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setContentType(excludedMimeType); - response.setBufferSize(10); - PrintWriter writer = response.getWriter(); - writer.println("a string larger than the buffer size"); - writer.println("Committed: " + response.isCommitted()); - writer.println("NumFiles: " + getNumFiles()); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The response was committed after the first write and we never created a file to buffer the response into. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, containsString("Committed: true")); - assertThat(responseContent, containsString("NumFiles: 0")); - assertThat(getNumFiles(), is(0)); - } - - @Test - public void testFlushed() throws Exception - { - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(1024); - PrintWriter writer = response.getWriter(); - writer.println("a string smaller than the buffer size"); - writer.println("NumFilesBeforeFlush: " + getNumFiles()); - writer.flush(); - writer.println("Committed: " + response.isCommitted()); - writer.println("NumFiles: " + getNumFiles()); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The response was not committed after the buffer was flushed and a file was created to buffer the response. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, containsString("NumFilesBeforeFlush: 0")); - assertThat(responseContent, containsString("Committed: false")); - assertThat(responseContent, containsString("NumFiles: 1")); - - // Unable to verify file deletion on windows, as immediate delete not possible. - // only after a GC has occurred. - if (!OS.WINDOWS.isCurrentOs()) - { - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - } - - @Test - public void testClosed() throws Exception - { - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(10); - PrintWriter writer = response.getWriter(); - writer.println("a string larger than the buffer size"); - writer.println("NumFiles: " + getNumFiles()); - writer.close(); - writer.println("writtenAfterClose"); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The content written after close was not sent. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, not(containsString("writtenAfterClose"))); - assertThat(responseContent, containsString("NumFiles: 1")); - - // Unable to verify file deletion on windows, as immediate delete not possible. - // only after a GC has occurred. - if (!OS.WINDOWS.isCurrentOs()) - { - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - } - - @Test - public void testBufferSizeBig() throws Exception - { - int bufferSize = 4096; - String largeContent = generateContent(bufferSize - 64); - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(bufferSize); - PrintWriter writer = response.getWriter(); - writer.println(largeContent); - writer.println("Committed: " + response.isCommitted()); - writer.println("NumFiles: " + getNumFiles()); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The content written was not buffered as a file as it was less than the buffer size. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, not(containsString("writtenAfterClose"))); - assertThat(responseContent, containsString("Committed: false")); - assertThat(responseContent, containsString("NumFiles: 0")); - assertThat(getNumFiles(), is(0)); - } - - @Test - public void testFlushEmpty() throws Exception - { - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(1024); - PrintWriter writer = response.getWriter(); - writer.flush(); - int numFiles = getNumFiles(); - writer.println("NumFiles: " + numFiles); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // The flush should not create the file unless there is content to write. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, containsString("NumFiles: 0")); - - // Unable to verify file deletion on windows, as immediate delete not possible. - // only after a GC has occurred. - if (!OS.WINDOWS.isCurrentOs()) - { - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - } - - @Test - public void testReset() throws Exception - { - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - response.setBufferSize(8); - PrintWriter writer = response.getWriter(); - writer.println("THIS WILL BE RESET"); - writer.flush(); - writer.println("THIS WILL BE RESET"); - int numFilesBeforeReset = getNumFiles(); - response.resetBuffer(); - int numFilesAfterReset = getNumFiles(); - - writer.println("NumFilesBeforeReset: " + numFilesBeforeReset); - writer.println("NumFilesAfterReset: " + numFilesAfterReset); - writer.println("a string larger than the buffer size"); - writer.println("NumFilesAfterWrite: " + getNumFiles()); - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - String responseContent = response.getContent(); - - // Resetting the response buffer will delete the file. - assertThat(response.getStatus(), is(HttpStatus.OK_200)); - assertThat(responseContent, not(containsString("THIS WILL BE RESET"))); - - assertThat(responseContent, containsString("NumFilesBeforeReset: 1")); - assertThat(responseContent, containsString("NumFilesAfterReset: 0")); - assertThat(responseContent, containsString("NumFilesAfterWrite: 1")); - - // Unable to verify file deletion on windows, as immediate delete not possible. - // only after a GC has occurred. - if (!OS.WINDOWS.isCurrentOs()) - { - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - } - - @Test - public void testFileLargerThanMaxInteger() throws Exception - { - long fileSize = Integer.MAX_VALUE + 1234L; - byte[] bytes = randomBytes(1024 * 1024); - - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - ServletOutputStream outputStream = response.getOutputStream(); - - long written = 0; - while (written < fileSize) - { - int length = Math.toIntExact(Math.min(bytes.length, fileSize - written)); - outputStream.write(bytes, 0, length); - written += length; - } - outputStream.flush(); - - response.getHeaders().put("NumFiles", Integer.toString(getNumFiles())); - response.getHeaders().put("FileSize", Long.toString(getFileSize())); - } - }); - - _server.start(); - - AtomicLong received = new AtomicLong(); - HttpTester.Response response = new HttpTester.Response() - { - @Override - public boolean content(ByteBuffer ref) - { - // Verify the content is what was sent. - while (ref.hasRemaining()) - { - byte byteFromBuffer = ref.get(); - long totalReceived = received.getAndIncrement(); - int bytesIndex = (int)(totalReceived % bytes.length); - byte byteFromArray = bytes[bytesIndex]; - - if (byteFromBuffer != byteFromArray) - { - LOG.warn("Mismatch at index {} received bytes {}, {}!={}", bytesIndex, totalReceived, byteFromBuffer, byteFromArray, new IllegalStateException()); - return true; - } - } - - return false; - } - }; - - try (Socket socket = new Socket("localhost", _serverConnector.getLocalPort())) - { - OutputStream output = socket.getOutputStream(); - String request = "GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"; - output.write(request.getBytes(StandardCharsets.UTF_8)); - output.flush(); - - HttpTester.Input input = HttpTester.from(socket.getInputStream()); - HttpTester.parseResponse(input, response); - } - - assertTrue(response.isComplete()); - assertThat(response.get("NumFiles"), is("1")); - assertThat(response.get("FileSize"), is(Long.toString(fileSize))); - assertThat(received.get(), is(fileSize)); - - // Unable to verify file deletion on windows, as immediate delete not possible. - // only after a GC has occurred. - if (!OS.WINDOWS.isCurrentOs()) - { - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - } - - @Test - public void testNextInterceptorFailed() throws Exception - { - AbstractHandler failingInterceptorHandler = new AbstractHandler() - { - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - HttpOutput httpOutput = baseRequest.getResponse().getHttpOutput(); - HttpOutput.Interceptor nextInterceptor = httpOutput.getInterceptor(); - httpOutput.setInterceptor(new HttpOutput.Interceptor() - { - @Override - public void write(ByteBuffer content, boolean last, Callback callback) - { - callback.failed(new Throwable("intentionally throwing from interceptor")); - } - - @Override - public HttpOutput.Interceptor getNextInterceptor() - { - return nextInterceptor; - } - }); - } - }; - - _server.setHandler(new HandlerCollection(failingInterceptorHandler, _server.getHandler())); - CompletableFuture errorFuture = new CompletableFuture<>(); - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - byte[] chunk1 = "this content will ".getBytes(); - byte[] chunk2 = "be buffered in a file".getBytes(); - response.setContentLength(chunk1.length + chunk2.length); - ServletOutputStream outputStream = response.getOutputStream(); - - // Write chunk1 and then flush so it is written to the file. - outputStream.write(chunk1); - outputStream.flush(); - assertThat(getNumFiles(), is(1)); - - try - { - // ContentLength is set so it knows this is the last write. - // This will cause the file to be written to the next interceptor which will fail. - outputStream.write(chunk2); - } - catch (Throwable t) - { - errorFuture.complete(t); - throw t; - } - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - - // Response was aborted. - assertThat(response.getStatus(), is(0)); - - // We failed because of the next interceptor. - Throwable error = errorFuture.get(5, TimeUnit.SECONDS); - assertThat(error.getMessage(), containsString("intentionally throwing from interceptor")); - - // Unable to verify file deletion on windows, as immediate delete not possible. - // only after a GC has occurred. - if (!OS.WINDOWS.isCurrentOs()) - { - // All files were deleted. - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - } - - @Test - public void testFileWriteFailed() throws Exception - { - // Set the temp directory to an empty directory so that the file cannot be created. - File tempDir = MavenTestingUtils.getTargetTestingDir(getClass().getSimpleName()); - FS.ensureDeleted(tempDir); - _bufferedHandler.setTempDir(tempDir.toPath()); - - CompletableFuture errorFuture = new CompletableFuture<>(); - _bufferedHandler.setHandler(new Handler.Abstract() - { - @Override - public void handle(request request, Response response, Callback callback) throws Exception - { - ServletOutputStream outputStream = response.getOutputStream(); - byte[] content = "this content will be buffered in a file".getBytes(); - - try - { - // Write the content and flush it to the file. - // This should throw as it cannot create the file to aggregate into. - outputStream.write(content); - outputStream.flush(); - } - catch (Throwable t) - { - errorFuture.complete(t); - throw t; - } - } - }); - - _server.start(); - String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n"); - HttpTester.Response response = HttpTester.parseResponse(rawResponse); - - // Response was aborted. - assertThat(response.getStatus(), is(0)); - - // We failed because cannot create the file. - Throwable error = errorFuture.get(5, TimeUnit.SECONDS); - assertThat(error, instanceOf(NoSuchFileException.class)); - - // No files were created. - assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS)); - assertThat(getNumFiles(), is(0)); - } - - */ - - private int getNumFiles() - { - File[] files = _testDir.toFile().listFiles(); - if (files == null) - return 0; - - return files.length; - } - - private long getFileSize() - { - File[] files = _testDir.toFile().listFiles(); - assertNotNull(files); - assertThat(files.length, is(1)); - return files[0].length(); - } - - private static String generateContent(int size) - { - Random random = new Random(); - StringBuilder stringBuilder = new StringBuilder(size); - for (int i = 0; i < size; i++) - { - stringBuilder.append((char)Math.abs(random.nextInt(0x7F))); - } - return stringBuilder.toString(); - } - - @SuppressWarnings("SameParameterValue") - private byte[] randomBytes(int size) - { - byte[] data = new byte[size]; - new Random().nextBytes(data); - return data; - } -} From c8100b18b8ae67ef6a23d3c808f5121dff1ed153 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Thu, 29 Jun 2023 14:41:04 +0200 Subject: [PATCH 58/63] Update to bundle-plugin-5.1.9 (#9954) --- jetty-runner/pom.xml | 2 -- pom.xml | 2 +- tests/jetty-jmh/pom.xml | 18 ++++++++++++++++++ .../src/main/resources/META-INF/MANIFEST.MF | 0 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 tests/jetty-jmh/src/main/resources/META-INF/MANIFEST.MF diff --git a/jetty-runner/pom.xml b/jetty-runner/pom.xml index 53aef30615e..99224114637 100644 --- a/jetty-runner/pom.xml +++ b/jetty-runner/pom.xml @@ -102,8 +102,6 @@ true - - ${project.build.directory}/NON_USED_MANIFEST diff --git a/pom.xml b/pom.xml index e14ea7ee15d..4e25d8254b9 100644 --- a/pom.xml +++ b/pom.xml @@ -138,7 +138,7 @@ 4.1 3.1.0 3.6.0 - 5.1.8 + 5.1.9 3.2.0 3.3.0 3.11.0 diff --git a/tests/jetty-jmh/pom.xml b/tests/jetty-jmh/pom.xml index c8a079ff83f..6f57423c339 100644 --- a/tests/jetty-jmh/pom.xml +++ b/tests/jetty-jmh/pom.xml @@ -57,6 +57,24 @@ + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + + + Jetty Jmh Support Classes + + + + + + diff --git a/tests/jetty-jmh/src/main/resources/META-INF/MANIFEST.MF b/tests/jetty-jmh/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..e69de29bb2d From 55d0f9445b4d1aab450e65d964985c14034b5a27 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 29 Jun 2023 11:40:42 -0500 Subject: [PATCH 59/63] Issue #9973 - Unwrap URI.scheme-specific-parts until we find a path we can resolve against. (#9995) * Issue #9973 - Unwrap URI.scheme-specific-parts until we find a path we can resolve against. --- .../util/resource/URLResourceFactory.java | 24 ++++++++++++-- .../util/resource/UrlResourceFactoryTest.java | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java index 243a3ad8fa0..66f5b234792 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java @@ -25,6 +25,7 @@ import java.nio.file.Path; import java.time.Instant; import org.eclipse.jetty.util.FileID; +import org.eclipse.jetty.util.URIUtil; /** * {@link ResourceFactory} for {@link java.net.URL} based resources. @@ -122,7 +123,7 @@ public class URLResourceFactory implements ResourceFactory @Override public URI getURI() { - return uri; + return URIUtil.correctFileURI(uri); } @Override @@ -140,7 +141,7 @@ public class URLResourceFactory implements ResourceFactory @Override public Resource resolve(String subUriPath) { - URI newURI = uri.resolve(subUriPath); + URI newURI = resolve(uri, subUriPath); try { return new URLResource(newURI, this.connectTimeout, this.useCaches); @@ -151,6 +152,25 @@ public class URLResourceFactory implements ResourceFactory } } + // This could probably live in URIUtil, but it's awefully specific to URLResourceFactory. + private static URI resolve(URI parent, String path) + { + if (parent.isOpaque() && parent.getPath() == null) + { + URI resolved = resolve(URI.create(parent.getRawSchemeSpecificPart()), path); + return URI.create(parent.getScheme() + ":" + resolved.toASCIIString()); + } + else if (parent.getPath() != null) + { + return parent.resolve(path); + } + else + { + // Not possible to use URLs that without a path in Jetty. + throw new RuntimeException("URL without path not supported by Jetty: " + parent); + } + } + @Override public boolean exists() { diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java index 0fed754b86c..93f172161f6 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java @@ -15,6 +15,7 @@ package org.eclipse.jetty.util.resource; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.nio.ByteBuffer; @@ -26,6 +27,7 @@ import java.time.Instant; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.URIUtil; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -121,6 +123,37 @@ public class UrlResourceFactoryTest } } + @Test + public void testResolveUri() throws MalformedURLException + { + Path path = MavenTestingUtils.getTestResourcePath("example.jar"); + URI jarFileUri = URI.create("jar:" + path.toUri().toASCIIString() + "!/WEB-INF/"); + + URLResourceFactory urlResourceFactory = new URLResourceFactory(); + Resource resource = urlResourceFactory.newResource(jarFileUri.toURL()); + + Resource webResource = resource.resolve("web.xml"); + assertThat(webResource.isDirectory(), is(false)); + URI expectedURI = URI.create(jarFileUri.toASCIIString() + "web.xml"); + assertThat(webResource.getURI(), is(expectedURI)); + } + + @Test + public void testResolveUriNoPath() throws MalformedURLException + { + Path path = MavenTestingUtils.getTestResourcePath("example.jar"); + URI jarFileUri = URI.create("file:" + path.toUri().toASCIIString()); + + URLResourceFactory urlResourceFactory = new URLResourceFactory(); + Resource resource = urlResourceFactory.newResource(jarFileUri.toURL()); + + Resource webResource = resource.resolve("web.xml"); + assertThat(webResource.isDirectory(), is(false)); + + URI expectedURI = URIUtil.correctFileURI(URI.create("file:" + path.toUri().resolve("web.xml").toASCIIString())); + assertThat(webResource.getURI(), is(expectedURI)); + } + private static long fileSize(URL url) throws IOException { try (InputStream is = url.openStream()) From 17c593f9eab9cc1624c9e8bc571cc337fbf0347d Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 30 Jun 2023 05:22:36 -0500 Subject: [PATCH 60/63] No progress during Gzip Request Inflation results in bogus error (#9997) * Issue #9990 - GzipHttpInputInterceptor doesn't decompress properly on some sized content. Signed-off-by: Joakim Erdfelt --- .../jetty/server/AsyncContentProducer.java | 19 -- .../server/AsyncContentProducerTest.java | 38 ---- .../server/BlockingContentProducerTest.java | 27 --- .../jetty/servlet/GzipHandlerInputTest.java | 178 ++++++++++++++++++ 4 files changed, 178 insertions(+), 84 deletions(-) create mode 100644 jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java index 6cf2581e0fb..b3141c79941 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java @@ -427,7 +427,6 @@ class AsyncContentProducer implements ContentProducer { try { - int remainingBeforeInterception = _rawContent.remaining(); HttpInput.Content content = _interceptor.readFrom(_rawContent); if (content != null && content.isSpecial() && !_rawContent.isSpecial()) { @@ -444,24 +443,6 @@ class AsyncContentProducer implements ContentProducer if (LOG.isDebugEnabled()) LOG.debug("interceptor generated special content {}", this); } - else if (content != _rawContent && !_rawContent.isSpecial() && !_rawContent.isEmpty() && _rawContent.remaining() == remainingBeforeInterception) - { - IOException failure = new IOException("Interceptor " + _interceptor + " did not consume any of the " + _rawContent.remaining() + " remaining byte(s) of content"); - if (content != null) - content.failed(failure); - failCurrentContent(failure); - // Set the _error flag to mark the content as definitive, i.e.: - // do not try to produce new raw content to get a fresher error - // when the special content was caused by the interceptor not - // consuming the raw content. - _error = true; - Response response = _httpChannel.getResponse(); - if (response.isCommitted()) - _httpChannel.abort(failure); - if (LOG.isDebugEnabled()) - LOG.debug("interceptor did not consume content {}", this); - content = _transformedContent; - } if (LOG.isDebugEnabled()) LOG.debug("intercepted raw content {}", this); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncContentProducerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncContentProducerTest.java index 3b094c5fbb1..0ec726589c2 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncContentProducerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncContentProducerTest.java @@ -282,44 +282,6 @@ public class AsyncContentProducerTest assertThat(contentFailedCount.get(), is(1)); } - @Test - public void testAsyncContentProducerInterceptorDoesNotConsume() - { - AtomicInteger contentFailedCount = new AtomicInteger(); - AtomicInteger interceptorContentFailedCount = new AtomicInteger(); - ContentProducer contentProducer = new AsyncContentProducer(new ContentListHttpChannel(List.of(new HttpInput.Content(ByteBuffer.allocate(1)) - { - @Override - public void failed(Throwable x) - { - contentFailedCount.incrementAndGet(); - } - }), new HttpInput.EofContent())); - try (AutoLock lock = contentProducer.lock()) - { - contentProducer.setInterceptor(content -> new HttpInput.Content(ByteBuffer.allocate(1)) - { - @Override - public void failed(Throwable x) - { - interceptorContentFailedCount.incrementAndGet(); - } - }); - - assertThat(contentProducer.isReady(), is(true)); - - HttpInput.Content content1 = contentProducer.nextContent(); - assertThat(content1.isSpecial(), is(true)); - assertThat(content1.getError().getMessage(), endsWith("did not consume any of the 1 remaining byte(s) of content")); - - HttpInput.Content content2 = contentProducer.nextContent(); - assertThat(content2.isSpecial(), is(true)); - assertThat(content2.getError().getMessage(), endsWith("did not consume any of the 1 remaining byte(s) of content")); - } - assertThat(contentFailedCount.get(), is(1)); - assertThat(interceptorContentFailedCount.get(), is(1)); - } - @Test public void testAsyncContentProducerInterceptorDoesNotConsumeEmptyContent() { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingContentProducerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingContentProducerTest.java index 1623ffbf7ef..fc54dbcb282 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingContentProducerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingContentProducerTest.java @@ -279,33 +279,6 @@ public class BlockingContentProducerTest assertThat(contentFailedCount.get(), is(1)); } - @Test - public void testBlockingContentProducerInterceptorDoesNotConsume() - { - AtomicInteger contentFailedCount = new AtomicInteger(); - ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(new StaticContentHttpChannel(new HttpInput.Content(ByteBuffer.allocate(1)) - { - @Override - public void failed(Throwable x) - { - contentFailedCount.incrementAndGet(); - } - }))); - try (AutoLock lock = contentProducer.lock()) - { - contentProducer.setInterceptor(content -> null); - - HttpInput.Content content1 = contentProducer.nextContent(); - assertThat(content1.isSpecial(), is(true)); - assertThat(content1.getError().getMessage(), endsWith("did not consume any of the 1 remaining byte(s) of content")); - - HttpInput.Content content2 = contentProducer.nextContent(); - assertThat(content2.isSpecial(), is(true)); - assertThat(content2.getError().getMessage(), endsWith("did not consume any of the 1 remaining byte(s) of content")); - } - assertThat(contentFailedCount.get(), is(1)); - } - @Test public void testBlockingContentProducerInterceptorDoesNotConsumeEmptyContent() { diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java new file mode 100644 index 00000000000..662d6ffc89f --- /dev/null +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java @@ -0,0 +1,178 @@ +// +// ======================================================================== +// 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.servlet; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import java.util.zip.GZIPOutputStream; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.BytesRequestContent; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Tests of GzipHandler behavior with gzip compressed Request content. + */ +public class GzipHandlerInputTest +{ + private Server server; + private HttpClient client; + + @BeforeEach + public void init() throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setInflateBufferSize(8192); // enable request inflation + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/"); + servletContextHandler.addServlet(ReadAllInputServlet.class, "/inflate"); + + gzipHandler.setHandler(servletContextHandler); + server.setHandler(gzipHandler); + server.start(); + + client = new HttpClient(); + client.start(); + } + + @AfterEach + public void stop() + { + LifeCycle.stop(server); + LifeCycle.stop(client); + } + + public static Stream transferScenarios() + { + int[] sizes = { + 0, 1, 8191, 8192, 8193, 8194, 8195, 8226, 8227, 8260, 8261, 8262, 8263, 8264, + 8192, 8193, 8194, 8195, 8226, 8227, 8228, 8259, 8260, 8261, 8262, 8263, 8515, + 8516, 8517, 8518, 8773, 8774, 8775, 9216 + }; + List scenarios = new ArrayList<>(); + // Scenarios 1: use Content-Length on request + for (int size : sizes) + { + scenarios.add(Arguments.of(size, true)); + } + // Scenarios 2: use Transfer-Encoding: chunked on request + for (int size : sizes) + { + scenarios.add(Arguments.of(size, false)); + } + return scenarios.stream(); + } + + @ParameterizedTest + @MethodSource("transferScenarios") + public void testReadGzippedInput(int testLength, boolean sendContentLength) throws Exception + { + byte[] rawBuf = new byte[testLength]; + Arrays.fill(rawBuf, (byte)'x'); + + byte[] gzipBuf; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzipOut = new GZIPOutputStream(baos)) + { + gzipOut.write(rawBuf, 0, rawBuf.length); + gzipOut.flush(); + gzipOut.finish(); + gzipBuf = baos.toByteArray(); + } + + URI destURI = server.getURI().resolve("/inflate"); + BytesRequestContent bytesRequestContent = new BytesRequestContent(gzipBuf, new byte[0]) + { + @Override + public long getLength() + { + if (sendContentLength) + return super.getLength(); + return -1; // we want chunked transfer-encoding + } + }; + Request request = client.newRequest(destURI) + .method(HttpMethod.POST) + .headers((headers) -> headers.put(HttpHeader.CONTENT_ENCODING, "gzip")) + .body(bytesRequestContent); + ContentResponse response = request.send(); + + assertThat(response.getStatus(), is(200)); + String responseBody = response.getContentAsString(); + if (sendContentLength) + assertThat(responseBody, containsString(String.format("[X-Content-Length]: %d", gzipBuf.length))); + else + assertThat(responseBody, containsString("[Transfer-Encoding]: chunked")); + + assertThat(responseBody, containsString("[X-Content-Encoding]: gzip")); + assertThat(responseBody, containsString(String.format("Read %d bytes", rawBuf.length))); + } + + public static class ReadAllInputServlet extends HttpServlet + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + InputStream input = req.getInputStream(); + byte[] buf = input.readAllBytes(); + resp.setCharacterEncoding("utf-8"); + resp.setContentType("text/plain"); + + PrintWriter out = resp.getWriter(); + // dump header names & values + List headerNames = Collections.list(req.getHeaderNames()); + Collections.sort(headerNames); + for (String headerName : headerNames) + { + List headerValues = Collections.list(req.getHeaders(headerName)); + out.printf("header [%s]: %s%n", headerName, String.join(", ", headerValues)); + } + // dump number of bytes read + out.printf("Read %d bytes%n", buf.length); + } + } +} From 998b394ff69e5cb54b39e5b38036193dc8075bbb Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 30 Jun 2023 05:46:11 -0500 Subject: [PATCH 61/63] Correcting imports --- .../org/eclipse/jetty/servlet/GzipHandlerInputTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java index 662d6ffc89f..d2765639031 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerInputTest.java @@ -24,11 +24,11 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; From 457d41ccaddff9ba44a86ffc22c8c432fe61a31c Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Fri, 30 Jun 2023 13:15:45 +0200 Subject: [PATCH 62/63] Add constructors accepting the handler to wrap to all core handler wrappers (#9988) Add constructors accepting the handler to wrap to all core handler wrappers Signed-off-by: Ludovic Orban --- .../old_docs/extras/default-handler.adoc | 10 +- .../server/http/server-http-handler-use.adoc | 8 +- .../server/http/HTTPServerDocs.java | 156 ++++++------------ .../jetty/rewrite/handler/RewriteHandler.java | 13 +- .../jetty/security/SecurityHandler.java | 12 ++ .../handler/BufferedResponseHandler.java | 6 + .../jetty/server/handler/ConnectHandler.java | 2 +- .../jetty/server/handler/ContextHandler.java | 13 +- .../jetty/server/handler/DebugHandler.java | 10 ++ .../jetty/server/handler/DelayedHandler.java | 10 ++ .../jetty/server/handler/EventsHandler.java | 1 + .../jetty/server/handler/GracefulHandler.java | 6 + .../server/handler/IdleTimeoutHandler.java | 10 ++ .../server/handler/InetAccessHandler.java | 10 ++ .../jetty/server/handler/ResourceHandler.java | 10 ++ .../handler/SecuredRedirectHandler.java | 25 ++- .../jetty/server/handler/ShutdownHandler.java | 8 +- .../server/handler/StatisticsHandler.java | 8 +- .../server/handler/ThreadLimitHandler.java | 11 +- .../jetty/server/handler/TryPathsHandler.java | 10 ++ .../server/handler/gzip/GzipHandler.java | 10 ++ .../server/handler/StatisticsHandlerTest.java | 4 +- 22 files changed, 218 insertions(+), 135 deletions(-) diff --git a/documentation/jetty-documentation/src/main/asciidoc/old_docs/extras/default-handler.adoc b/documentation/jetty-documentation/src/main/asciidoc/old_docs/extras/default-handler.adoc index d9693b3c4dd..78258fb5299 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/old_docs/extras/default-handler.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/old_docs/extras/default-handler.adoc @@ -31,17 +31,17 @@ There is also an link:#error-handler[Error Handler] that services errors related _____ [NOTE] -The `DefaultHandler` will also handle serving out the `flav.ico` file should a request make it through all of the other handlers without being resolved. +The `DefaultHandler` will also handle serving out the `favicon.ico` file should a request make it through all of the other handlers without being resolved. _____ [source, java, subs="{sub-order}"] ---- Server server = new Server(8080); - HandlerList handlers = new HandlerList(); ResourceHandler resourceHandler = new ResourceHandler(); - resourceHandler.setBaseResource(Resource.newResource(".")); - handlers.setHandlers(new Handler[] - { resourceHandler, new DefaultHandler() }); + resourceHandler.setBaseResource(ResourceFactory.of(resourceHandler).newResource(".")); + Handler.Sequence handlers = new Handler.Sequence( + resourceHandler, new DefaultHandler() + ); server.setHandler(handlers); server.start(); ---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc index c9c07bb1200..b0e1a62da87 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc @@ -46,7 +46,7 @@ The Jetty Server Libraries provides a number of out-of-the-box __Handler__s that ====== ContextHandler `ContextHandler` is a `Handler` that represents a _context_ for a web application. -It is a `HandlerWrapper` that performs some action before and after delegating to the nested `Handler`. +It is a `Handler.Wrapper` that performs some action before and after delegating to the nested `Handler`. // TODO: expand on what the ContextHandler does, e.g. ServletContext. The simplest use of `ContextHandler` is the following: @@ -140,7 +140,7 @@ See also xref:pg-server-http-handler-use-util-default-handler[how to use] `Defau `GzipHandler` provides supports for automatic decompression of compressed request content and automatic compression of response content. -`GzipHandler` is a `HandlerWrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content. +`GzipHandler` is a `Handler.Wrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content. The decompression/compression is not performed until the web application reads request content or writes response content. `GzipHandler` can be configured at the server level in this way: @@ -291,7 +291,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer * Sends a HTTP `404` response for any other request * The HTTP `404` response content nicely shows a HTML table with all the contexts deployed on the `Server` instance -`DefaultHandler` is best used as the last `Handler` of a `HandlerList`, for example: +`DefaultHandler` is best used directly set on the server, for example: [source,java,indent=0] ---- @@ -310,7 +310,7 @@ Server └── DefaultHandler ---- -In the example above, `ContextHandlerCollection` will try to match a request to one of the contexts; if the match fails, `HandlerList` will call the next `Handler` which is `DefaultHandler` that will return a HTTP `404` with an HTML page showing the existing contexts deployed on the `Server`. +In the example above, `ContextHandlerCollection` will try to match a request to one of the contexts; if the match fails, `Server` will call the `DefaultHandler` that will return a HTTP `404` with an HTML page showing the existing contexts deployed on the `Server`. NOTE: `DefaultHandler` just sends a nicer HTTP `404` response in case of wrong requests from clients. Jetty will send an HTTP `404` response anyway if `DefaultHandler` is not used. diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java index fcc5b0b6b13..58a4efc1ec6 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -110,53 +110,6 @@ public class HTTPServerDocs // end::simple[] } - public void httpChannelListener() throws Exception - { - // tag::httpChannelListener[] - // TODO: HttpChannelState.Listener does not exist anymore. -/* - class TimingHttpChannelListener implements HttpChannelState.Listener - { - private final ConcurrentMap times = new ConcurrentHashMap<>(); - - @Override - public void onRequestBegin(Request request) - { - times.put(request, NanoTime.now()); - } - - @Override - public void onComplete(Request request) - { - long begin = times.remove(request); - long elapsed = NanoTime.since(begin); - System.getLogger("timing").log(INFO, "Request {0} took {1} ns", request, elapsed); - } - } - - Server server = new Server(); - - Connector connector = new ServerConnector(server); - server.addConnector(connector); - - // Add the HttpChannel.Listener as bean to the connector. - connector.addBean(new TimingHttpChannelListener()); - - // Set a simple Handler to handle requests/responses. - server.setHandler(new AbstractHandler() - { - @Override - public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) - { - jettyRequest.setHandled(true); - } - }); - - server.start(); -*/ - // end::httpChannelListener[] - } - public void serverRequestLogSLF4J() { // tag::serverRequestLogSLF4J[] @@ -508,14 +461,14 @@ public class HTTPServerDocs // tag::handlerTree[] Server server = new Server(); - GzipHandler gzipHandler = new GzipHandler(); - server.setHandler(gzipHandler); - Handler.Sequence sequence = new Handler.Sequence(); - gzipHandler.setHandler(sequence); - sequence.addHandler(new App1Handler()); sequence.addHandler(new App2Handler()); + + GzipHandler gzipHandler = new GzipHandler(sequence); + + server.setHandler(gzipHandler); + // end::handlerTree[] } @@ -585,6 +538,11 @@ public class HTTPServerDocs // tag::handlerFilter[] class FilterHandler extends Handler.Wrapper { + public FilterHandler(Handler handler) + { + super(handler); + } + @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { @@ -617,9 +575,7 @@ public class HTTPServerDocs server.addConnector(connector); // Link the Handlers. - FilterHandler filter = new FilterHandler(); - filter.setHandler(new HelloWorldHandler()); - server.setHandler(filter); + server.setHandler(new FilterHandler(new HelloWorldHandler())); server.start(); // end::handlerFilter[] @@ -643,9 +599,7 @@ public class HTTPServerDocs server.addConnector(connector); // Create a ContextHandler with contextPath. - ContextHandler context = new ContextHandler(); - context.setContextPath("/shop"); - context.setHandler(new ShopHandler()); + ContextHandler context = new ContextHandler(new ShopHandler(), "/shop"); // Link the context to the server. server.setHandler(context); @@ -683,20 +637,17 @@ public class HTTPServerDocs // Create a ContextHandlerCollection to hold contexts. ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + + // Create the context for the shop web application and add it to ContextHandlerCollection. + contextCollection.addHandler(new ContextHandler(new ShopHandler(), "/shop")); + // Link the ContextHandlerCollection to the Server. server.setHandler(contextCollection); - // Create the context for the shop web application. - ContextHandler shopContext = new ContextHandler("/shop"); - shopContext.setHandler(new ShopHandler()); - // Add it to ContextHandlerCollection. - contextCollection.addHandler(shopContext); - server.start(); // Create the context for the API web application. - ContextHandler apiContext = new ContextHandler("/api"); - apiContext.setHandler(new RESTHandler()); + ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api"); // Web applications can be deployed after the Server is started. contextCollection.deployHandler(apiContext, Callback.NOOP); // end::contextHandlerCollection[] @@ -791,7 +742,7 @@ public class HTTPServerDocs // tag::multipleResourcesHandler[] ResourceHandler handler = new ResourceHandler(); - // For multiple directories, use ResourceCollection. + // For multiple directories, use ResourceFactory.combine(). Resource resource = ResourceFactory.combine( ResourceFactory.of(handler).newResource("/path/to/static/resources/"), ResourceFactory.of(handler).newResource("/another/path/to/static/resources/") @@ -822,8 +773,10 @@ public class HTTPServerDocs Connector connector = new ServerConnector(server); server.addConnector(connector); - // Create and configure GzipHandler. - GzipHandler gzipHandler = new GzipHandler(); + // Create a ContextHandlerCollection to manage contexts. + ContextHandlerCollection contexts = new ContextHandlerCollection(); + // Create and configure GzipHandler linked to the ContextHandlerCollection. + GzipHandler gzipHandler = new GzipHandler(contexts); // Only compress response content larger than this. gzipHandler.setMinGzipSize(1024); // Do not compress these URI paths. @@ -833,10 +786,6 @@ public class HTTPServerDocs // Do not compress these mime types. gzipHandler.addExcludedMimeTypes("font/ttf"); - // Link a ContextHandlerCollection to manage contexts. - ContextHandlerCollection contexts = new ContextHandlerCollection(); - gzipHandler.setHandler(contexts); - // Link the GzipHandler to the Server. server.setHandler(gzipHandler); @@ -873,26 +822,21 @@ public class HTTPServerDocs // tag::contextGzipHandler[] // Create a ContextHandlerCollection to hold contexts. ContextHandlerCollection contextCollection = new ContextHandlerCollection(); - // Link the ContextHandlerCollection to the Server. - server.setHandler(contextCollection); - // Create the context for the shop web application. - ContextHandler shopContext = new ContextHandler("/shop"); - shopContext.setHandler(new ShopHandler()); - - // You want to gzip the shop web application only. - GzipHandler shopGzipHandler = new GzipHandler(); - shopGzipHandler.setHandler(shopContext); + // Create the context for the shop web application wrapped with GzipHandler so only the shop will do gzip. + GzipHandler shopGzipHandler = new GzipHandler(new ContextHandler(new ShopHandler(), "/shop")); // Add it to ContextHandlerCollection. contextCollection.addHandler(shopGzipHandler); // Create the context for the API web application. - ContextHandler apiContext = new ContextHandler("/api"); - apiContext.setHandler(new RESTHandler()); + ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api"); // Add it to ContextHandlerCollection. contextCollection.addHandler(apiContext); + + // Link the ContextHandlerCollection to the Server. + server.setHandler(contextCollection); // end::contextGzipHandler[] server.start(); @@ -905,7 +849,10 @@ public class HTTPServerDocs ServerConnector connector = new ServerConnector(server); server.addConnector(connector); - RewriteHandler rewriteHandler = new RewriteHandler(); + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + // Link the ContextHandlerCollection to the RewriteHandler. + RewriteHandler rewriteHandler = new RewriteHandler(contextCollection); // Compacts URI paths with double slashes, e.g. /ctx//path/to//resource. rewriteHandler.addRule(new CompactPathRule()); // Rewrites */products/* to */p/*. @@ -918,11 +865,6 @@ public class HTTPServerDocs // Link the RewriteHandler to the Server. server.setHandler(rewriteHandler); - // Create a ContextHandlerCollection to hold contexts. - ContextHandlerCollection contextCollection = new ContextHandlerCollection(); - // Link the ContextHandlerCollection to the RewriteHandler. - rewriteHandler.setHandler(contextCollection); - server.start(); // end::rewriteHandler[] } @@ -934,16 +876,15 @@ public class HTTPServerDocs ServerConnector connector = new ServerConnector(server); server.addConnector(connector); - StatisticsHandler statsHandler = new StatisticsHandler(); + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + + // Link the ContextHandlerCollection to the StatisticsHandler. + StatisticsHandler statsHandler = new StatisticsHandler(contextCollection); // Link the StatisticsHandler to the Server. server.setHandler(statsHandler); - // Create a ContextHandlerCollection to hold contexts. - ContextHandlerCollection contextCollection = new ContextHandlerCollection(); - // Link the ContextHandlerCollection to the StatisticsHandler. - statsHandler.setHandler(contextCollection); - server.start(); // end::statisticsHandler[] } @@ -955,17 +896,15 @@ public class HTTPServerDocs ServerConnector connector = new ServerConnector(server); server.addConnector(connector); - // Create the MinimumDataRateHandler with a minimum read rate of 1KB per second and no minimum write rate. - StatisticsHandler.MinimumDataRateHandler dataRateHandler = new StatisticsHandler.MinimumDataRateHandler(1024L, 0L); + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + + // Create the MinimumDataRateHandler linked the ContextHandlerCollection with a minimum read rate of 1KB per second and no minimum write rate. + StatisticsHandler.MinimumDataRateHandler dataRateHandler = new StatisticsHandler.MinimumDataRateHandler(contextCollection, 1024L, 0L); // Link the MinimumDataRateHandler to the Server. server.setHandler(dataRateHandler); - // Create a ContextHandlerCollection to hold contexts. - ContextHandlerCollection contextCollection = new ContextHandlerCollection(); - // Link the ContextHandlerCollection to the MinimumDataRateHandler. - dataRateHandler.setHandler(contextCollection); - server.start(); // end::dataRateHandler[] } @@ -1006,16 +945,15 @@ public class HTTPServerDocs secureConnector.setPort(8443); server.addConnector(secureConnector); - SecuredRedirectHandler securedHandler = new SecuredRedirectHandler(); + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + + // Link the ContextHandlerCollection to the SecuredRedirectHandler. + SecuredRedirectHandler securedHandler = new SecuredRedirectHandler(contextCollection); // Link the SecuredRedirectHandler to the Server. server.setHandler(securedHandler); - // Create a ContextHandlerCollection to hold contexts. - ContextHandlerCollection contextCollection = new ContextHandlerCollection(); - // Link the ContextHandlerCollection to the StatisticsHandler. - securedHandler.setHandler(contextCollection); - server.start(); // end::securedHandler[] } diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java index 76a51ef1114..de291971720 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java @@ -43,11 +43,22 @@ public class RewriteHandler extends Handler.Wrapper public RewriteHandler() { - this(new RuleContainer()); + this(null, new RuleContainer()); } public RewriteHandler(RuleContainer rules) { + this(null, rules); + } + + public RewriteHandler(Handler handler) + { + this(handler, new RuleContainer()); + } + + public RewriteHandler(Handler handler, RuleContainer rules) + { + super(handler); _rules = rules; addBean(_rules); } diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java index 3fce929ff5a..584b235041b 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java @@ -87,6 +87,12 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu protected SecurityHandler() { + this(null); + } + + protected SecurityHandler(Handler handler) + { + super(handler); addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories)); } @@ -645,6 +651,12 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu public PathMapped() { + this(null); + } + + public PathMapped(Handler handler) + { + super(handler); } public Constraint put(String pathSpec, Constraint constraint) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java index b93bbd2f6e4..26f51c0c2a6 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java @@ -65,6 +65,12 @@ public class BufferedResponseHandler extends Handler.Wrapper public BufferedResponseHandler() { + this(null); + } + + public BufferedResponseHandler(Handler handler) + { + super(handler); _methods.include(HttpMethod.GET.asString()); for (String type : MimeTypes.DEFAULTS.getMimeMap().values()) { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java index 50aab932fda..b70497e8116 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java @@ -77,7 +77,7 @@ public class ConnectHandler extends Handler.Wrapper public ConnectHandler(Handler handler) { - setHandler(handler); + super(handler); } public Executor getExecutor() diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 1d47f5e6232..dbbf00910b7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -159,11 +159,22 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias public ContextHandler() { - this(null); + this(null, null); + } + + public ContextHandler(Handler handler) + { + this(handler, null); } public ContextHandler(String contextPath) { + this(null, contextPath); + } + + public ContextHandler(Handler handler, String contextPath) + { + super(handler); _context = newContext(); if (contextPath != null) setContextPath(contextPath); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java index a797fa1df5a..9fdca439fe9 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java @@ -41,6 +41,16 @@ public class DebugHandler extends Handler.Wrapper implements Connection.Listener private OutputStream _out; private PrintStream _print; + public DebugHandler() + { + this(null); + } + + public DebugHandler(Handler handler) + { + super(handler); + } + @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { 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 90ecf7449b9..5f681da33ec 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 @@ -38,6 +38,16 @@ import org.eclipse.jetty.util.StringUtil; public class DelayedHandler extends Handler.Wrapper { + public DelayedHandler() + { + this(null); + } + + public DelayedHandler(Handler handler) + { + super(handler); + } + @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java index c272e941438..147b4b074f6 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java @@ -55,6 +55,7 @@ public abstract class EventsHandler extends Handler.Wrapper public EventsHandler() { + this(null); } public EventsHandler(Handler handler) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/GracefulHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/GracefulHandler.java index 99565a435db..d5ac4735edd 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/GracefulHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/GracefulHandler.java @@ -39,6 +39,12 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful public GracefulHandler() { + this(null); + } + + public GracefulHandler(Handler handler) + { + super(handler); _shutdown = new Shutdown(this) { @Override diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IdleTimeoutHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IdleTimeoutHandler.java index e7a4d10c002..485aafada21 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IdleTimeoutHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/IdleTimeoutHandler.java @@ -36,6 +36,16 @@ public class IdleTimeoutHandler extends Handler.Wrapper private long _idleTimeoutMs = 1000; private boolean _applyToAsync = false; + public IdleTimeoutHandler() + { + this(null); + } + + public IdleTimeoutHandler(Handler handler) + { + super(handler); + } + public boolean isApplyToAsync() { return _applyToAsync; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java index 1c385b54620..cd6ce518730 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java @@ -47,6 +47,16 @@ public class InetAccessHandler extends Handler.Wrapper private final IncludeExcludeSet _set = new IncludeExcludeSet<>(InetAccessSet.class); + public InetAccessHandler() + { + this(null); + } + + public InetAccessHandler(Handler handler) + { + super(handler); + } + /** * Clears all the includes, excludes, included connector names and excluded * connector names. diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java index 43dea077632..a9d2de48b35 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java @@ -63,6 +63,16 @@ public class ResourceHandler extends Handler.Wrapper private MimeTypes _mimeTypes; private List _welcomes = List.of("index.html"); + public ResourceHandler() + { + this(null); + } + + public ResourceHandler(Handler handler) + { + super(handler); + } + protected ResourceService newResourceService() { return new HandlerResourceService(); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java index 9218d32fc4f..10e9b4c3c06 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java @@ -47,7 +47,7 @@ public class SecuredRedirectHandler extends Handler.Wrapper */ public SecuredRedirectHandler() { - this(HttpStatus.MOVED_TEMPORARILY_302); + this(null, HttpStatus.MOVED_TEMPORARILY_302); } /** @@ -56,8 +56,29 @@ public class SecuredRedirectHandler extends Handler.Wrapper * @param code the redirect code to use in the response * @throws IllegalArgumentException if parameter is an invalid redirect code */ - public SecuredRedirectHandler(final int code) + public SecuredRedirectHandler(int code) { + this(null, code); + } + + /** + * Uses moved temporarily code (302) as the redirect code. + */ + public SecuredRedirectHandler(Handler handler) + { + this(handler, HttpStatus.MOVED_TEMPORARILY_302); + } + + /** + * Use supplied code as the redirect code. + * + * @param handler the handler to wrap + * @param code the redirect code to use in the response + * @throws IllegalArgumentException if parameter is an invalid redirect code + */ + public SecuredRedirectHandler(Handler handler, int code) + { + super(handler); if (!HttpStatus.isRedirection(code)) throw new IllegalArgumentException("Not a 3xx redirect code"); _redirectCode = code; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java index 1f0897ca938..69513f2f07a 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java @@ -91,7 +91,7 @@ public class ShutdownHandler extends Handler.Wrapper */ public ShutdownHandler(String shutdownToken) { - this(null, shutdownToken, false); + this(null, null, shutdownToken, false); } /** @@ -102,18 +102,20 @@ public class ShutdownHandler extends Handler.Wrapper */ public ShutdownHandler(String shutdownToken, boolean exitJVM) { - this(null, shutdownToken, exitJVM); + this(null, null, shutdownToken, exitJVM); } /** * Creates a Handler that lets the server be shut down remotely (but only from localhost). * + * @param handler the handler to wrap * @param shutdownPath the path to respond to shutdown requests against (default is "{@code /shutdown}") * @param shutdownToken a secret password to avoid unauthorized shutdown attempts * @param exitJVM If true, when the shutdown is executed, the handler class System.exit() */ - public ShutdownHandler(String shutdownPath, String shutdownToken, boolean exitJVM) + public ShutdownHandler(Handler handler, String shutdownPath, String shutdownToken, boolean exitJVM) { + super(handler); this._shutdownPath = StringUtil.isBlank(shutdownPath) ? "/shutdown" : shutdownPath; this._shutdownToken = shutdownToken; this._exitJvm = exitJVM; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java index 6cb7d4f3821..2de64a19c1b 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java @@ -241,17 +241,17 @@ public class StatisticsHandler extends EventsHandler */ public MinimumDataRateHandler(long minimumReadRate, long minimumWriteRate) { - _minimumReadRate = minimumReadRate; - _minimumWriteRate = minimumWriteRate; + this(null, minimumReadRate, minimumWriteRate); } /** * Creates a {@code MinimumDataRateHandler} with the specified read and write rates. + * + * @param handler the handler to wrap. * @param minimumReadRate the minimum number of bytes to be read per second, or 0 for not checking the read rate. * @param minimumWriteRate the minimum number of bytes to be written per second, or 0 for not checking the write rate. - * @param handler the handler to wrap. */ - public MinimumDataRateHandler(long minimumReadRate, long minimumWriteRate, Handler handler) + public MinimumDataRateHandler(Handler handler, long minimumReadRate, long minimumWriteRate) { super(handler); _minimumReadRate = minimumReadRate; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java index 3f11b882894..dbaa11433f8 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java @@ -80,17 +80,22 @@ public class ThreadLimitHandler extends Handler.Wrapper public ThreadLimitHandler() { - this(null, true); + this(null, null, true); } public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader) { - this(forwardedHeader, HttpHeader.FORWARDED.is(forwardedHeader)); + this(null, forwardedHeader, HttpHeader.FORWARDED.is(forwardedHeader)); } public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239) { - super(); + this(null, forwardedHeader, rfc7239); + } + + public ThreadLimitHandler(@Name("handler") Handler handler, @Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239) + { + super(handler); _rfc7239 = rfc7239; _forwardedHeader = forwardedHeader; _enabled = true; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java index 91129875eb2..f2354c74bbe 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java @@ -71,6 +71,16 @@ public class TryPathsHandler extends Handler.Wrapper private String originalQueryAttribute; private List paths; + public TryPathsHandler() + { + this(null); + } + + public TryPathsHandler(Handler handler) + { + super(handler); + } + /** * @return the attribute name of the original request path */ 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 f9e10e33dfe..e4fb737ddd0 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 @@ -63,6 +63,16 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory */ public GzipHandler() { + this(null); + } + + /** + * Instantiates a new GzipHandler. + * @param handler the handler to wrap + */ + public GzipHandler(Handler handler) + { + super(handler); _methods.include(HttpMethod.GET.asString()); _methods.include(HttpMethod.POST.asString()); for (String type : MimeTypes.DEFAULTS.getMimeMap().values()) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java index 17edad793c1..8927039f7f8 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java @@ -158,7 +158,7 @@ public class StatisticsHandlerTest AtomicReference exceptionRef = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); int expectedContentLength = 1000; - StatisticsHandler.MinimumDataRateHandler mdrh = new StatisticsHandler.MinimumDataRateHandler(0, 1000, new Handler.Abstract.NonBlocking() + StatisticsHandler.MinimumDataRateHandler mdrh = new StatisticsHandler.MinimumDataRateHandler(new Handler.Abstract.NonBlocking() { @Override public boolean handle(Request request, Response response, Callback callback) @@ -217,7 +217,7 @@ public class StatisticsHandlerTest response.write(true, ByteBuffer.allocate(1), finalCallback); } } - }); + }, 0, 1000); _latchHandler.setHandler(mdrh); _server.start(); From ec2dbe73a8fb29243820ad822074808c422d056c Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 30 Jun 2023 17:01:16 +0200 Subject: [PATCH 63/63] Fully async Multipart Form handling (#9975) A fully async ContentSourceCompletableFuture for use by MultiPartFormData and MultiPartByteRanges Restructure MultiPartFormData to have a Parser class --------- Signed-off-by: Simone Bordet Co-authored-by: Simone Bordet --- .../jetty/docs/programming/ContentDocs.java | 91 ++ .../util/MultiPartRequestContentTest.java | 6 +- .../jetty/http/MultiPartByteRanges.java | 228 +++-- .../eclipse/jetty/http/MultiPartFormData.java | 827 +++++++++--------- .../jetty/http/MultiPartFormDataTest.java | 238 ++--- .../ContentSourceCompletableFuture.java | 144 +++ .../org/eclipse/jetty/server/FormFields.java | 299 ++++--- .../jetty/server/handler/DelayedHandler.java | 102 --- .../eclipse/jetty/server/FormFieldsTest.java | 92 ++ .../jetty/server/MultiPartByteRangesTest.java | 5 +- .../handler/MultiPartFormDataHandlerTest.java | 110 +-- .../ResourceHandlerByteRangesTest.java | 2 +- .../server/handler/gzip/GzipHandlerTest.java | 5 +- .../QuickStartGeneratorConfiguration.java | 2 +- .../jetty/ee10/servlet/Dispatcher.java | 9 + .../jetty/ee10/servlet/EagerFormHandler.java | 83 ++ .../jetty/ee10/servlet/ServletApiRequest.java | 137 +-- .../ee10/servlet/ServletContextRequest.java | 10 + .../jetty/ee10/servlet/ServletHolder.java | 25 +- .../servlet/ServletMultiPartFormData.java | 186 ++-- .../ee10/servlet/MultiPartServletTest.java | 139 +-- .../webapp/StandardDescriptorProcessor.java | 2 +- 22 files changed, 1515 insertions(+), 1227 deletions(-) create mode 100644 jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceCompletableFuture.java create mode 100644 jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/FormFieldsTest.java create mode 100644 jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/EagerFormHandler.java diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java index aa3364180f5..3724c4480f8 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java @@ -13,11 +13,18 @@ package org.eclipse.jetty.docs.programming; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.content.AsyncContent; +import org.eclipse.jetty.io.content.ContentSourceCompletableFuture; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.CharsetStringBuilder; import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.Utf8StringBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -146,8 +153,92 @@ public class ContentDocs throw new IllegalStateException("EOF expected"); } + public static class FutureString extends CompletableFuture + { + private final CharsetStringBuilder text; + private final Content.Source source; + + public FutureString(Content.Source source, Charset charset) + { + this.source = source; + this.text = CharsetStringBuilder.forCharset(charset); + source.demand(this::onContentAvailable); + } + + private void onContentAvailable() + { + while (true) + { + Content.Chunk chunk = source.read(); + if (chunk == null) + { + source.demand(this::onContentAvailable); + return; + } + + try + { + if (Content.Chunk.isFailure(chunk)) + throw chunk.getFailure(); + + if (chunk.hasRemaining()) + text.append(chunk.getByteBuffer()); + + if (chunk.isLast() && complete(text.build())) + return; + } + catch (Throwable e) + { + completeExceptionally(e); + } + finally + { + chunk.release(); + } + } + } + } + + public static void testFutureString() throws Exception + { + AsyncContent source = new AsyncContent(); + FutureString future = new FutureString(source, StandardCharsets.UTF_8); + if (future.isDone()) + throw new IllegalStateException(); + + Callback.Completable writeCallback = new Callback.Completable(); + Content.Sink.write(source, false, "One", writeCallback); + if (!writeCallback.isDone() || future.isDone()) + throw new IllegalStateException("Should be consumed"); + Content.Sink.write(source, false, "Two", writeCallback); + if (!writeCallback.isDone() || future.isDone()) + throw new IllegalStateException("Should be consumed"); + Content.Sink.write(source, true, "Three", writeCallback); + if (!writeCallback.isDone() || !future.isDone()) + throw new IllegalStateException("Should be consumed"); + } + + public static class FutureUtf8String extends ContentSourceCompletableFuture + { + private final Utf8StringBuilder builder = new Utf8StringBuilder(); + + public FutureUtf8String(Content.Source content) + { + super(content); + } + + @Override + protected String parse(Content.Chunk chunk) throws Throwable + { + if (chunk.hasRemaining()) + builder.append(chunk.getByteBuffer()); + return chunk.isLast() ? builder.takeCompleteString(IllegalStateException::new) : null; + } + } + public static void main(String... args) throws Exception { testEcho(); + testFutureString(); } } diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartRequestContentTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartRequestContentTest.java index 1a03575314f..f6b598f848f 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartRequestContentTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartRequestContentTest.java @@ -439,12 +439,12 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); assertEquals("multipart/form-data", HttpField.valueParameters(contentType, null)); String boundary = MultiPart.extractBoundary(contentType); - MultiPartFormData formData = new MultiPartFormData(boundary); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary); formData.setFilesDirectory(tmpDir); - formData.parse(request); + try { - process(formData.join()); // May block waiting for multipart form data. + process(formData.parse(request).join()); // May block waiting for multipart form data. response.write(true, BufferUtil.EMPTY_BUFFER, callback); } catch (Exception x) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java index 944d9f1dee5..0efcc9aaf2b 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartByteRanges.java @@ -23,12 +23,12 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.content.ContentSourceCompletableFuture; import org.eclipse.jetty.util.thread.AutoLock; /** *

    A {@link CompletableFuture} that is completed when a multipart/byteranges - * content has been parsed asynchronously from a {@link Content.Source} via - * {@link #parse(Content.Source)}.

    + * has been parsed asynchronously from a {@link Content.Source}.

    *

    Once the parsing of the multipart/byteranges content completes successfully, * objects of this class are completed with a {@link MultiPartByteRanges.Parts} * object.

    @@ -52,75 +52,10 @@ import org.eclipse.jetty.util.thread.AutoLock; * * @see Parts */ -public class MultiPartByteRanges extends CompletableFuture +public class MultiPartByteRanges { - private final PartsListener listener = new PartsListener(); - private final MultiPart.Parser parser; - - public MultiPartByteRanges(String boundary) + private MultiPartByteRanges() { - this.parser = new MultiPart.Parser(boundary, listener); - } - - /** - * @return the boundary string - */ - public String getBoundary() - { - return parser.getBoundary(); - } - - @Override - public boolean completeExceptionally(Throwable failure) - { - listener.fail(failure); - return super.completeExceptionally(failure); - } - - /** - *

    Parses the given multipart/byteranges content.

    - *

    Returns this {@code MultiPartByteRanges} object, - * so that it can be used in the typical "fluent" style - * of {@link CompletableFuture}.

    - * - * @param content the multipart/byteranges content to parse - * @return this {@code MultiPartByteRanges} object - */ - public MultiPartByteRanges parse(Content.Source content) - { - new Runnable() - { - @Override - public void run() - { - while (true) - { - Content.Chunk chunk = content.read(); - if (chunk == null) - { - content.demand(this); - return; - } - if (Content.Chunk.isFailure(chunk)) - { - listener.onFailure(chunk.getFailure()); - return; - } - parse(chunk); - chunk.release(); - if (chunk.isLast() || isDone()) - return; - } - } - }.run(); - return this; - } - - private void parse(Content.Chunk chunk) - { - if (listener.isFailed()) - return; - parser.parse(chunk); } /** @@ -267,76 +202,123 @@ public class MultiPartByteRanges extends CompletableFuture partChunks = new ArrayList<>(); - private final List parts = new ArrayList<>(); - private Throwable failure; + private final PartsListener listener = new PartsListener(); + private final MultiPart.Parser parser; + private Parts parts; - private boolean isFailed() + public Parser(String boundary) { - try (AutoLock ignored = lock.lock()) - { - return failure != null; - } + parser = new MultiPart.Parser(boundary, listener); } - @Override - public void onPartContent(Content.Chunk chunk) + public CompletableFuture parse(Content.Source content) { - try (AutoLock ignored = lock.lock()) + ContentSourceCompletableFuture futureParts = new ContentSourceCompletableFuture<>(content) { - // Retain the chunk because it is stored for later use. - chunk.retain(); - partChunks.add(chunk); - } + @Override + protected MultiPartByteRanges.Parts parse(Content.Chunk chunk) throws Throwable + { + if (listener.isFailed()) + throw listener.failure; + parser.parse(chunk); + if (listener.isFailed()) + throw listener.failure; + return parts; + } + + @Override + public boolean completeExceptionally(Throwable failure) + { + boolean failed = super.completeExceptionally(failure); + if (failed) + listener.fail(failure); + return failed; + } + }; + futureParts.parse(); + return futureParts; } - @Override - public void onPart(String name, String fileName, HttpFields headers) + /** + * @return the boundary string + */ + public String getBoundary() { - try (AutoLock ignored = lock.lock()) - { - parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks))); - partChunks.forEach(Content.Chunk::release); - partChunks.clear(); - } + return parser.getBoundary(); } - @Override - public void onComplete() + private class PartsListener extends MultiPart.AbstractPartsListener { - super.onComplete(); - List copy; - try (AutoLock ignored = lock.lock()) - { - copy = List.copyOf(parts); - } - complete(new Parts(getBoundary(), copy)); - } + private final AutoLock lock = new AutoLock(); + private final List partChunks = new ArrayList<>(); + private final List parts = new ArrayList<>(); + private Throwable failure; - @Override - public void onFailure(Throwable failure) - { - super.onFailure(failure); - completeExceptionally(failure); - } - - private void fail(Throwable cause) - { - List partsToFail; - try (AutoLock ignored = lock.lock()) + private boolean isFailed() { - if (failure != null) - return; - failure = cause; - partsToFail = List.copyOf(parts); - parts.clear(); - partChunks.forEach(Content.Chunk::release); - partChunks.clear(); + try (AutoLock ignored = lock.lock()) + { + return failure != null; + } + } + + @Override + public void onPartContent(Content.Chunk chunk) + { + try (AutoLock ignored = lock.lock()) + { + // Retain the chunk because it is stored for later use. + chunk.retain(); + partChunks.add(chunk); + } + } + + @Override + public void onPart(String name, String fileName, HttpFields headers) + { + try (AutoLock ignored = lock.lock()) + { + parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks))); + partChunks.forEach(Content.Chunk::release); + partChunks.clear(); + } + } + + @Override + public void onComplete() + { + super.onComplete(); + List copy; + try (AutoLock ignored = lock.lock()) + { + copy = List.copyOf(parts); + Parser.this.parts = new Parts(getBoundary(), copy); + } + } + + @Override + public void onFailure(Throwable failure) + { + fail(failure); + } + + private void fail(Throwable cause) + { + List partsToFail; + try (AutoLock ignored = lock.lock()) + { + if (failure != null) + return; + failure = cause; + partsToFail = List.copyOf(parts); + parts.clear(); + partChunks.forEach(Content.Chunk::release); + partChunks.clear(); + } + partsToFail.forEach(p -> p.fail(cause)); } - partsToFail.forEach(p -> p.fail(cause)); } } } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java index 851e4c08551..1f58648e699 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java @@ -26,8 +26,11 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.content.ContentSourceCompletableFuture; +import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; @@ -37,8 +40,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII; /** *

    A {@link CompletableFuture} that is completed when a multipart/form-data content - * has been parsed asynchronously from a {@link Content.Source} via {@link #parse(Content.Source)} - * or from one or more {@link Content.Chunk}s via {@link #parse(Content.Chunk)}.

    + * has been parsed asynchronously from a {@link Content.Source}.

    *

    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 @@ import static java.nio.charset.StandardCharsets.US_ASCII; * * @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) + private MultiPartFormData() { - parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener); } - /** - * @return the boundary string - */ - public String getBoundary() + public static CompletableFuture from(Attributes attributes, String boundary, Function> parse) { - 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) - { - 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 @@ public class MultiPartFormData extends CompletableFutureReturns the {@link MultiPart.Part} at the given index, a number * between {@code 0} included and the value returned by {@link #size()} @@ -409,251 +196,447 @@ public class MultiPartFormData extends CompletableFuture 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; - @Override - public void onPartContent(Content.Chunk chunk) + public Parser(String boundary) { - ByteBuffer buffer = chunk.getByteBuffer(); - String fileName = getFileName(); - if (fileName != null || isUseFilesForPartsWithoutFileName()) + parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener); + } + + public CompletableFuture parse(Content.Source content) + { + 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) - { - try - { - // Must save to disk. - if (ensureFileChannel()) - { - // 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); - } - - try (AutoLock ignored = lock.lock()) - { - partChunks.forEach(Content.Chunk::release); - partChunks.clear(); - } - return; - } + boolean failed = super.completeExceptionally(failure); + if (failed) + listener.fail(failure); + return failed; } - } - // Retain the chunk because it is stored for later use. - chunk.retain(); - try (AutoLock ignored = lock.lock()) - { - partChunks.add(chunk); - } + }; + futureParts.parse(); + return futureParts; } - private void write(ByteBuffer buffer) throws Exception + /** + * @return the boundary string + */ + public String getBoundary() { - int remaining = buffer.remaining(); - while (remaining > 0) - { - SeekableByteChannel channel = fileChannel(); - if (channel == null) - throw new IllegalStateException(); - int written = channel.write(buffer); - if (written == 0) - throw new NonWritableChannelException(); - remaining -= written; - } + return parser.getBoundary(); } - private void close() + /** + *

    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() { - try - { - Closeable closeable = fileChannel(); - if (closeable != null) - closeable.close(); - } - catch (Throwable x) - { - onFailure(x); - } + return listener.getDefaultCharset(); } - @Override - public void onPart(String name, String fileName, HttpFields headers) + /** + * @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length + */ + public int getPartHeadersMaxLength() { - fileSize = 0; - memoryFileSize = 0; - try (AutoLock ignored = lock.lock()) - { - MultiPart.Part part; - if (fileChannel != null) - part = new MultiPart.PathPart(name, fileName, headers, filePath); - else - part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)); - // Reset part-related state. - filePath = null; - fileChannel = null; - partChunks.forEach(Content.Chunk::release); - partChunks.clear(); - // Store the new part. - parts.add(part); - } + return parser.getPartHeadersMaxLength(); } - @Override - public void onComplete() + /** + * @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length + */ + public void setPartHeadersMaxLength(int partHeadersMaxLength) { - super.onComplete(); - List result; - try (AutoLock ignored = lock.lock()) - { - result = List.copyOf(parts); - } - complete(new Parts(result)); + parser.setPartHeadersMaxLength(partHeadersMaxLength); } - Charset getDefaultCharset() + /** + * @return whether parts without fileName may be stored as files + */ + public boolean isUseFilesForPartsWithoutFileName() { - try (AutoLock ignored = lock.lock()) - { - return parts.stream() - .filter(part -> "_charset_".equals(part.getName())) - .map(part -> part.getContentAsString(US_ASCII)) - .map(Charset::forName) - .findFirst() - .orElse(null); - } + return useFilesForPartsWithoutFileName; } + /** + * @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files + */ + public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName) + { + this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName; + } + + /** + * @return the directory where files are saved + */ + public Path getFilesDirectory() + { + return filesDirectory; + } + + /** + *

    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() { - try (AutoLock ignored = lock.lock()) - { - return parts.size(); - } + return listener.getPartsSize(); } - @Override - public void onFailure(Throwable failure) + private class PartsListener extends MultiPart.AbstractPartsListener { - super.onFailure(failure); - completeExceptionally(failure); - } + private final AutoLock lock = new AutoLock(); + private final List 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 void fail(Throwable cause) - { - List partsToFail; - try (AutoLock ignored = lock.lock()) + @Override + public void onPartContent(Content.Chunk chunk) { - if (failure != null) - return; - failure = cause; - partsToFail = List.copyOf(parts); - parts.clear(); - partChunks.forEach(Content.Chunk::release); - partChunks.clear(); - } - partsToFail.forEach(p -> p.fail(cause)); - close(); - delete(); - } + ByteBuffer buffer = chunk.getByteBuffer(); + String fileName = getFileName(); + if (fileName != null || isUseFilesForPartsWithoutFileName()) + { + long maxFileSize = getMaxFileSize(); + fileSize += buffer.remaining(); + if (maxFileSize >= 0 && fileSize > maxFileSize) + { + onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize))); + return; + } - private SeekableByteChannel fileChannel() - { - try (AutoLock ignored = lock.lock()) - { - return fileChannel; - } - } + long maxMemoryFileSize = getMaxMemoryFileSize(); + if (maxMemoryFileSize >= 0) + { + memoryFileSize += buffer.remaining(); + if (memoryFileSize > maxMemoryFileSize) + { + try + { + // Must save to disk. + if (ensureFileChannel()) + { + // Write existing memory chunks. + List partChunks; + try (AutoLock ignored = lock.lock()) + { + partChunks = List.copyOf(this.partChunks); + } + for (Content.Chunk c : partChunks) + { + write(c.getByteBuffer()); + } + } + write(buffer); + if (chunk.isLast()) + close(); + } + catch (Throwable x) + { + onFailure(x); + } - private void delete() - { - try - { - Path path = null; + try (AutoLock ignored = lock.lock()) + { + partChunks.forEach(Content.Chunk::release); + partChunks.clear(); + } + return; + } + } + } + // Retain the chunk because it is stored for later use. + chunk.retain(); try (AutoLock ignored = lock.lock()) { - if (filePath != null) - path = filePath; + partChunks.add(chunk); + } + } + + private void write(ByteBuffer buffer) throws Exception + { + int remaining = buffer.remaining(); + while (remaining > 0) + { + SeekableByteChannel channel = fileChannel(); + if (channel == null) + throw new IllegalStateException(); + int written = channel.write(buffer); + if (written == 0) + throw new NonWritableChannelException(); + remaining -= written; + } + } + + private void close() + { + try + { + Closeable closeable = fileChannel(); + if (closeable != null) + closeable.close(); + } + catch (Throwable x) + { + onFailure(x); + } + } + + @Override + public void onPart(String name, String fileName, HttpFields headers) + { + fileSize = 0; + memoryFileSize = 0; + try (AutoLock ignored = lock.lock()) + { + MultiPart.Part part; + if (fileChannel != null) + part = new MultiPart.PathPart(name, fileName, headers, filePath); + else + part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)); + // Reset part-related state. filePath = null; fileChannel = null; + partChunks.forEach(Content.Chunk::release); + partChunks.clear(); + // Store the new part. + parts.add(part); } - if (path != null) - Files.delete(path); } - catch (Throwable x) - { - if (LOG.isTraceEnabled()) - LOG.trace("IGNORED", x); - } - } - private boolean isFailed() - { - try (AutoLock ignored = lock.lock()) + @Override + public void onComplete() { - return failure != null; + super.onComplete(); + List result; + try (AutoLock ignored = lock.lock()) + { + result = List.copyOf(parts); + Parser.this.parts = new Parts(result); + } } - } - private boolean ensureFileChannel() - { - try (AutoLock ignored = lock.lock()) + Charset getDefaultCharset() { - if (fileChannel != null) - return false; - createFileChannel(); - return true; + try (AutoLock ignored = lock.lock()) + { + return parts.stream() + .filter(part -> "_charset_".equals(part.getName())) + .map(part -> part.getContentAsString(US_ASCII)) + .map(Charset::forName) + .findFirst() + .orElse(null); + } } - } - private void createFileChannel() - { - try (AutoLock ignored = lock.lock()) + int getPartsSize() { - Path directory = getFilesDirectory(); - Files.createDirectories(directory); - String fileName = "MultiPart"; - filePath = Files.createTempFile(directory, fileName, ""); - fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND); + try (AutoLock ignored = lock.lock()) + { + return parts.size(); + } } - catch (Throwable x) + + @Override + public void onFailure(Throwable failure) { - onFailure(x); + fail(failure); + } + + private void fail(Throwable cause) + { + List partsToFail; + try (AutoLock ignored = lock.lock()) + { + if (failure != null) + return; + failure = cause; + partsToFail = List.copyOf(parts); + parts.clear(); + partChunks.forEach(Content.Chunk::release); + partChunks.clear(); + } + partsToFail.forEach(p -> p.fail(cause)); + close(); + delete(); + } + + private SeekableByteChannel fileChannel() + { + try (AutoLock ignored = lock.lock()) + { + return fileChannel; + } + } + + private void delete() + { + try + { + Path path = null; + try (AutoLock ignored = lock.lock()) + { + if (filePath != null) + path = filePath; + filePath = null; + fileChannel = null; + } + if (path != null) + Files.delete(path); + } + catch (Throwable x) + { + if (LOG.isTraceEnabled()) + LOG.trace("IGNORED", x); + } + } + + private boolean isFailed() + { + try (AutoLock ignored = lock.lock()) + { + return failure != null; + } + } + + private boolean ensureFileChannel() + { + try (AutoLock ignored = lock.lock()) + { + if (fileChannel != null) + return false; + createFileChannel(); + return true; + } + } + + private void createFileChannel() + { + try (AutoLock ignored = lock.lock()) + { + Path directory = getFilesDirectory(); + Files.createDirectories(directory); + String fileName = "MultiPart"; + filePath = Files.createTempFile(directory, fileName, ""); + fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND); + } + catch (Throwable x) + { + onFailure(x); + } } } } 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 a1f771f1ca2..5ccff5406a6 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 @@ package org.eclipse.jetty.http; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.content.AsyncContent; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -71,32 +72,19 @@ public class MultiPartFormDataTest int leaks = 0; for (Content.Chunk chunk : _allocatedChunks) { - // Any release that does not return true is a leak. - if (!chunk.release()) - leaks++; + // Any release that does not throw or return true is a leak. + try + { + if (!chunk.release()) + leaks++; + } + catch (IllegalStateException ignored) + { + } } assertThat("Leaked " + leaks + "/" + _allocatedChunks.size() + " chunk(s)", leaks, is(0)); } - Content.Chunk asChunk(String data, boolean last) - { - byte[] b = data.getBytes(StandardCharsets.UTF_8); - ByteBuffer buffer = BufferUtil.allocate(b.length); - BufferUtil.append(buffer, b); - Content.Chunk chunk = Content.Chunk.from(buffer, last); - _allocatedChunks.add(chunk); - return chunk; - } - - Content.Chunk asChunk(ByteBuffer data, boolean last) - { - ByteBuffer buffer = BufferUtil.allocate(data.remaining()); - BufferUtil.append(buffer, data); - Content.Chunk chunk = Content.Chunk.from(buffer, last); - _allocatedChunks.add(chunk); - return chunk; - } - @Test public void testBadMultiPart() throws Exception { @@ -109,14 +97,14 @@ public class MultiPartFormDataTest "Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" + "\r\n"; - MultiPartFormData formData = new MultiPartFormData(boundary); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary); formData.setFilesDirectory(_tmpDir); formData.setMaxFileSize(1024); formData.setMaxLength(3072); formData.setMaxMemoryFileSize(50); - formData.parse(asChunk(str, true)); - - formData.handle((parts, failure) -> + Content.Sink.write(source, true, str, Callback.NOOP); + formData.parse(source).handle((parts, failure) -> { assertNull(parts); assertInstanceOf(BadMessageException.class, failure); @@ -139,14 +127,14 @@ public class MultiPartFormDataTest eol + "--" + boundary + "--" + eol; - MultiPartFormData formData = new MultiPartFormData(boundary); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary); formData.setFilesDirectory(_tmpDir); formData.setMaxFileSize(1024); formData.setMaxLength(3072); formData.setMaxMemoryFileSize(50); - formData.parse(asChunk(str, true)); - - formData.whenComplete((parts, failure) -> + Content.Sink.write(source, true, str, Callback.NOOP); + formData.parse(source).whenComplete((parts, failure) -> { // No errors and no parts. assertNull(failure); @@ -165,14 +153,14 @@ public class MultiPartFormDataTest String str = eol + "--" + boundary + "--" + eol; - MultiPartFormData formData = new MultiPartFormData(boundary); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary); formData.setFilesDirectory(_tmpDir); formData.setMaxFileSize(1024); formData.setMaxLength(3072); formData.setMaxMemoryFileSize(50); - formData.parse(asChunk(str, true)); - - formData.whenComplete((parts, failure) -> + Content.Sink.write(source, true, str, Callback.NOOP); + formData.parse(source).whenComplete((parts, failure) -> { // No errors and no parts. assertNull(failure); @@ -213,14 +201,14 @@ public class MultiPartFormDataTest ----\r """; - MultiPartFormData formData = new MultiPartFormData(""); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser(""); formData.setFilesDirectory(_tmpDir); formData.setMaxFileSize(1024); formData.setMaxLength(3072); formData.setMaxMemoryFileSize(50); - formData.parse(asChunk(str, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, str, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertThat(parts.size(), is(4)); @@ -253,10 +241,10 @@ public class MultiPartFormDataTest @Test public void testNoBody() throws Exception { - MultiPartFormData formData = new MultiPartFormData("boundary"); - formData.parse(Content.Chunk.EOF); - - formData.handle((parts, failure) -> + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary"); + source.close(); + formData.parse(source).handle((parts, failure) -> { assertNull(parts); assertNotNull(failure); @@ -268,11 +256,11 @@ public class MultiPartFormDataTest @Test public void testBodyWithOnlyCRLF() throws Exception { - MultiPartFormData formData = new MultiPartFormData("boundary"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary"); String body = " \n\n\n\r\n\r\n\r\n\r\n"; - formData.parse(asChunk(body, true)); - - formData.handle((parts, failure) -> + Content.Sink.write(source, true, body, Callback.NOOP); + formData.parse(source).handle((parts, failure) -> { assertNull(parts); assertNotNull(failure); @@ -285,7 +273,7 @@ public class MultiPartFormDataTest public void testLeadingWhitespaceBodyWithCRLF() throws Exception { String body = """ - + \r \r @@ -303,14 +291,14 @@ public class MultiPartFormDataTest --AaB03x--\r """; - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxFileSize(1024); formData.setMaxLength(3072); formData.setMaxMemoryFileSize(50); - formData.parse(asChunk(body, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, body, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertThat(parts.size(), is(2)); MultiPart.Part part1 = parts.getFirst("field1"); @@ -340,14 +328,14 @@ public class MultiPartFormDataTest --AaB03x--\r """; - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxFileSize(1024); formData.setMaxLength(3072); formData.setMaxMemoryFileSize(50); - formData.parse(asChunk(body, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, body, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { // The first boundary must be on a new line, so the first "part" is not recognized as such. assertThat(parts.size(), is(1)); @@ -361,7 +349,8 @@ public class MultiPartFormDataTest @Test public void testDefaultLimits() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); String body = """ --AaB03x\r @@ -371,9 +360,8 @@ public class MultiPartFormDataTest ABCDEFGHIJKLMNOPQRSTUVWXYZ\r --AaB03x--\r """; - formData.parse(asChunk(body, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, body, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertThat(parts.size(), is(1)); MultiPart.Part part = parts.get(0); @@ -390,7 +378,8 @@ public class MultiPartFormDataTest @Test public void testRequestContentTooBig() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxLength(16); @@ -402,9 +391,8 @@ public class MultiPartFormDataTest ABCDEFGHIJKLMNOPQRSTUVWXYZ\r --AaB03x--\r """; - formData.parse(asChunk(body, true)); - - formData.handle((parts, failure) -> + Content.Sink.write(source, true, body, Callback.NOOP); + formData.parse(source).handle((parts, failure) -> { assertNull(parts); assertNotNull(failure); @@ -416,7 +404,8 @@ public class MultiPartFormDataTest @Test public void testFileTooBig() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxFileSize(16); @@ -428,9 +417,8 @@ public class MultiPartFormDataTest ABCDEFGHIJKLMNOPQRSTUVWXYZ\r --AaB03x--\r """; - formData.parse(asChunk(body, true)); - - formData.handle((parts, failure) -> + Content.Sink.write(source, true, body, Callback.NOOP); + formData.parse(source).handle((parts, failure) -> { assertNull(parts); assertNotNull(failure); @@ -442,7 +430,8 @@ public class MultiPartFormDataTest @Test public void testTwoFilesOneInMemoryOneOnDisk() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; formData.setMaxMemoryFileSize(chunk.length() + 1); @@ -460,9 +449,8 @@ public class MultiPartFormDataTest $C$C$C$C\r --AaB03x--\r """.replace("$C", chunk); - formData.parse(asChunk(body, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, body, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertNotNull(parts); assertEquals(2, parts.size()); @@ -482,7 +470,8 @@ public class MultiPartFormDataTest @Test public void testPartWrite() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; formData.setMaxMemoryFileSize(chunk.length() + 1); @@ -500,9 +489,8 @@ public class MultiPartFormDataTest $C$C$C$C\r --AaB03x--\r """.replace("$C", chunk); - formData.parse(asChunk(body, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, body, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertNotNull(parts); assertEquals(2, parts.size()); @@ -528,7 +516,8 @@ public class MultiPartFormDataTest @Test public void testPathPartDelete() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); String body = """ @@ -539,9 +528,8 @@ public class MultiPartFormDataTest ABCDEFGHIJKLMNOPQRSTUVWXYZ\r --AaB03x--\r """; - formData.parse(asChunk(body, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, body, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertNotNull(parts); assertEquals(1, parts.size()); @@ -559,7 +547,8 @@ public class MultiPartFormDataTest @Test public void testAbort() { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxMemoryFileSize(32); @@ -575,24 +564,27 @@ public class MultiPartFormDataTest --AaB03x--\r """; // Parse only part of the content. - formData.parse(asChunk(body, false)); + Content.Sink.write(source, false, body, Callback.NOOP); + + CompletableFuture futureParts = formData.parse(source); assertEquals(1, formData.getPartsSize()); // Abort MultiPartFormData. - formData.completeExceptionally(new IOException()); + futureParts.completeExceptionally(new IOException()); // Parse the rest of the content. - formData.parse(asChunk(terminator, true)); + Content.Sink.write(source, true, terminator, Callback.NOOP); // Try to get the parts, it should fail. - assertThrows(ExecutionException.class, () -> formData.get(5, TimeUnit.SECONDS)); + assertThrows(ExecutionException.class, () -> futureParts.get(5, TimeUnit.SECONDS)); assertEquals(0, formData.getPartsSize()); } @Test public void testMaxHeaderLength() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setPartHeadersMaxLength(32); @@ -604,9 +596,8 @@ public class MultiPartFormDataTest ABCDEFGHIJKLMNOPQRSTUVWXYZ\r --AaB03x--\r """; - formData.parse(asChunk(body, true)); - - formData.handle((parts, failure) -> + Content.Sink.write(source, true, body, Callback.NOOP); + formData.parse(source).handle((parts, failure) -> { assertNull(parts); assertNotNull(failure); @@ -618,7 +609,8 @@ public class MultiPartFormDataTest @Test public void testDefaultCharset() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxMemoryFileSize(-1); @@ -645,13 +637,14 @@ public class MultiPartFormDataTest \r --AaB03x--\r """; - formData.parse(asChunk(body1, false)); - formData.parse(asChunk(isoCedilla, false)); - formData.parse(asChunk(body2, false)); - formData.parse(asChunk(utfCedilla, false)); - formData.parse(asChunk(terminator, true)); + CompletableFuture futureParts = formData.parse(source); + Content.Sink.write(source, false, body1, Callback.NOOP); + source.write(false, isoCedilla, Callback.NOOP); + Content.Sink.write(source, false, body2, Callback.NOOP); + source.write(false, utfCedilla, Callback.NOOP); + Content.Sink.write(source, true, terminator, Callback.NOOP); - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS)) { Charset defaultCharset = formData.getDefaultCharset(); assertEquals(ISO_8859_1, defaultCharset); @@ -669,7 +662,8 @@ public class MultiPartFormDataTest @Test public void testPartWithBackSlashInFileName() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxMemoryFileSize(-1); @@ -681,9 +675,9 @@ public class MultiPartFormDataTest stuffaaa\r --AaB03x--\r """; - formData.parse(asChunk(contents, true)); + Content.Sink.write(source, true, contents, Callback.NOOP); - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertThat(parts.size(), is(1)); MultiPart.Part part = parts.get(0); @@ -694,7 +688,8 @@ public class MultiPartFormDataTest @Test public void testPartWithWindowsFileName() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxMemoryFileSize(-1); @@ -706,9 +701,8 @@ public class MultiPartFormDataTest stuffaaa\r --AaB03x--\r """; - formData.parse(asChunk(contents, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, contents, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertThat(parts.size(), is(1)); MultiPart.Part part = parts.get(0); @@ -722,7 +716,8 @@ public class MultiPartFormDataTest @Disabled public void testCorrectlyEncodedMSFilename() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setMaxMemoryFileSize(-1); @@ -734,9 +729,8 @@ public class MultiPartFormDataTest stuffaaa\r --AaB03x--\r """; - formData.parse(asChunk(contents, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, contents, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertThat(parts.size(), is(1)); MultiPart.Part part = parts.get(0); @@ -747,7 +741,8 @@ public class MultiPartFormDataTest @Test public void testWriteFilesForPartWithoutFileName() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); formData.setUseFilesForPartsWithoutFileName(true); @@ -759,9 +754,8 @@ public class MultiPartFormDataTest sssaaa\r --AaB03x--\r """; - formData.parse(asChunk(body, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, body, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertThat(parts.size(), is(1)); MultiPart.Part part = parts.get(0); @@ -775,7 +769,8 @@ public class MultiPartFormDataTest @Test public void testPartsWithSameName() throws Exception { - MultiPartFormData formData = new MultiPartFormData("AaB03x"); + AsyncContent source = new TestContent(); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x"); formData.setFilesDirectory(_tmpDir); String sameNames = """ @@ -791,9 +786,8 @@ public class MultiPartFormDataTest AAAAA\r --AaB03x--\r """; - formData.parse(asChunk(sameNames, true)); - - try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS)) + Content.Sink.write(source, true, sameNames, Callback.NOOP); + try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS)) { assertEquals(2, parts.size()); @@ -810,4 +804,16 @@ public class MultiPartFormDataTest assertEquals("AAAAA", part2.getContentAsString(formData.getDefaultCharset())); } } + + private class TestContent extends AsyncContent + { + @Override + public Content.Chunk read() + { + Content.Chunk chunk = super.read(); + if (chunk != null && chunk.canRetain()) + _allocatedChunks.add(chunk); + return chunk; + } + } } 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 00000000000..f44d9283261 --- /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 1f7f949add1..eaf96ccb5b8 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 java.util.concurrent.CompletableFuture; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.content.ContentSourceCompletableFuture; +import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.CharsetStringBuilder; import org.eclipse.jetty.util.Fields; @@ -33,7 +35,7 @@ import static org.eclipse.jetty.util.UrlEncoded.decodeHexByte; * A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded} * content has been parsed asynchronously from the {@link Content.Source}. */ -public class FormFields extends CompletableFuture 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,29 +59,22 @@ public class FormFields extends CompletableFuture implements Runnable return StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs); } - 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); - } - - 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); - } - + /** + * 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()); @@ -88,26 +83,93 @@ public class FormFields extends CompletableFuture implements Runnable return EMPTY; } + /** + * Find or create a {@link FormFields} from a {@link Content.Source}. + * @param request The {@link Request} in which to look for an existing {@link FormFields} attribute, + * using the classname as the attribute name, else the request is used + * as a {@link Content.Source} from which to read the fields and set the attribute. + * @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure. + * @see #from(Content.Source, Attributes, Charset, int, int) + */ + public static CompletableFuture from(Request request) + { + int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE); + int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE); + return from(request, maxFields, maxLength); + } + + /** + * Find or create a {@link FormFields} from a {@link Content.Source}. + * @param request The {@link Request} in which to look for an existing {@link FormFields} attribute, + * using the classname as the attribute name, else the request is used + * as a {@link Content.Source} from which to read the fields and set the attribute. + * @param charset the {@link Charset} to use for byte to string conversion. + * @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure. + * @see #from(Content.Source, Attributes, Charset, int, int) + */ + public static CompletableFuture from(Request request, Charset charset) + { + int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE); + int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE); + return from(request, charset, maxFields, maxLength); + } + + /** + * Find or create a {@link FormFields} from a {@link Content.Source}. + * @param request The {@link Request} in which to look for an existing {@link FormFields} attribute, + * using the classname as the attribute name, else the request is used + * as a {@link Content.Source} from which to read the fields and set the attribute. + * @param maxFields The maximum number of fields to be parsed + * @param maxLength The maximum total size of the fields + * @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure. + * @see #from(Content.Source, Attributes, Charset, int, int) + */ public static CompletableFuture from(Request request, int maxFields, int maxLength) { - Object attr = request.getAttribute(FormFields.class.getName()); + return from(request, getFormEncodedCharset(request), maxFields, maxLength); + } + + /** + * Find or create a {@link FormFields} from a {@link Content.Source}. + * @param request The {@link Request} in which to look for an existing {@link FormFields} attribute, + * using the classname as the attribute name, else the request is used + * as a {@link Content.Source} from which to read the fields and set the attribute. + * @param charset the {@link Charset} to use for byte to string conversion. + * @param maxFields The maximum number of fields to be parsed + * @param maxLength The maximum total size of the fields + * @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure. + * @see #from(Content.Source, Attributes, Charset, int, int) + */ + public static CompletableFuture from(Request request, Charset charset, int maxFields, int maxLength) + { + return from(request, request, charset, maxFields, maxLength); + } + + /** + * Find or create a {@link FormFields} from a {@link Content.Source}. + * @param source The {@link Content.Source} from which to read the fields. + * @param attributes The {@link Attributes} in which to look for an existing {@link CompletableFuture} of + * {@link FormFields}, using the classname as the attribute name. If not found the attribute + * is set with the created {@link CompletableFuture} of {@link FormFields}. + * @param charset the {@link Charset} to use for byte to string conversion. + * @param maxFields The maximum number of fields to be parsed + * @param maxLength The maximum total size of the fields + * @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure. + */ + static CompletableFuture from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength) + { + Object attr = attributes.getAttribute(FormFields.class.getName()); if (attr instanceof FormFields futureFormFields) return futureFormFields; else if (attr instanceof Fields fields) return CompletableFuture.completedFuture(fields); - Charset charset = getFormEncodedCharset(request); if (charset == null) return EMPTY; - return from(request, charset, maxFields, maxLength); - } - - public static CompletableFuture from(Request request, Charset charset, int maxFields, int maxLength) - { - FormFields futureFormFields = new FormFields(request, charset, maxFields, maxLength); - request.setAttribute(FormFields.class.getName(), futureFormFields); - futureFormFields.run(); + FormFields futureFormFields = new FormFields(source, charset, maxFields, maxLength); + attributes.setAttribute(FormFields.class.getName(), futureFormFields); + futureFormFields.parse(); return futureFormFields; } @@ -126,7 +188,6 @@ public class FormFields extends CompletableFuture implements Runnable } } - private final Content.Source _source; private final Fields _fields; private final CharsetStringBuilder _builder; private final int _maxFields; @@ -136,9 +197,9 @@ public class FormFields extends CompletableFuture implements Runnable private int _percent = 0; private byte _percentCode; - public FormFields(Content.Source source, Charset charset, int maxFields, int maxSize) + private FormFields(Content.Source source, Charset charset, int maxFields, int maxSize) { - _source = source; + super(source); _maxFields = maxFields; _maxLength = maxSize; _builder = CharsetStringBuilder.forCharset(charset); @@ -146,137 +207,91 @@ public class FormFields extends CompletableFuture implements Runnable } @Override - public void run() - { - Content.Chunk chunk = null; - try - { - while (true) - { - chunk = _source.read(); - if (chunk == null) - { - _source.demand(this); - return; - } - - if (Content.Chunk.isFailure(chunk)) - { - completeExceptionally(chunk.getFailure()); - return; - } - - while (true) - { - Fields.Field field = parse(chunk); - if (field == null) - break; - if (_maxFields >= 0 && _fields.getSize() >= _maxFields) - { - chunk.release(); - // Do not double release if completeExceptionally() throws. - chunk = null; - completeExceptionally(new IllegalStateException("form with too many fields")); - return; - } - _fields.add(field); - } - - chunk.release(); - if (chunk.isLast()) - { - // Do not double release if complete() throws. - chunk = null; - complete(_fields); - return; - } - } - } - catch (Throwable x) - { - if (chunk != null) - chunk.release(); - completeExceptionally(x); - } - } - - protected Fields.Field parse(Content.Chunk chunk) throws CharacterCodingException + protected Fields parse(Content.Chunk chunk) throws CharacterCodingException { String value = null; ByteBuffer buffer = chunk.getByteBuffer(); - loop: - while (BufferUtil.hasContent(buffer)) + + do { - byte b = buffer.get(); - switch (_percent) + loop: + while (BufferUtil.hasContent(buffer)) { - case 1 -> + byte b = buffer.get(); + switch (_percent) { - _percentCode = b; - _percent++; - continue; + case 1 -> + { + _percentCode = b; + _percent++; + continue; + } + case 2 -> + { + _builder.append(decodeHexByte((char)_percentCode, (char)b)); + _percent = 0; + continue; + } } - case 2 -> + + if (_name == null) { - _builder.append(decodeHexByte((char)_percentCode, (char)b)); - _percent = 0; - continue; + switch (b) + { + case '=' -> + { + _name = _builder.build(); + checkLength(_name); + } + case '+' -> _builder.append((byte)' '); + case '%' -> _percent++; + default -> _builder.append(b); + } + } + else + { + switch (b) + { + case '&' -> + { + value = _builder.build(); + checkLength(value); + break loop; + } + case '+' -> _builder.append((byte)' '); + case '%' -> _percent++; + default -> _builder.append(b); + } } } - if (_name == null) + if (_name != null) { - switch (b) + if (value == null && chunk.isLast()) { - case '=' -> + if (_percent > 0) { - _name = _builder.build(); - checkLength(_name); + _builder.append((byte)'%'); + _builder.append(_percentCode); } - case '+' -> _builder.append((byte)' '); - case '%' -> _percent++; - default -> _builder.append(b); + value = _builder.build(); + checkLength(value); } - } - else - { - switch (b) + + if (value != null) { - case '&' -> - { - value = _builder.build(); - checkLength(value); - break loop; - } - case '+' -> _builder.append((byte)' '); - case '%' -> _percent++; - default -> _builder.append(b); + Fields.Field field = new Fields.Field(_name, value); + _name = null; + value = null; + if (_maxFields >= 0 && _fields.getSize() >= _maxFields) + throw new IllegalStateException("form with too many fields > " + _maxFields); + _fields.add(field); } } } + while (BufferUtil.hasContent(buffer)); - if (_name != null) - { - if (value == null && chunk.isLast()) - { - if (_percent > 0) - { - _builder.append((byte)'%'); - _builder.append(_percentCode); - } - value = _builder.build(); - checkLength(value); - } - - if (value != null) - { - Fields.Field field = new Fields.Field(_name, value); - _name = null; - return field; - } - } - - return null; + return chunk.isLast() ? _fields : null; } private void checkLength(String nameOrValue) 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 5f681da33ec..a978b054f20 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.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; -import org.eclipse.jetty.http.MultiPart; -import org.eclipse.jetty.http.MultiPartFormData; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; @@ -115,7 +112,6 @@ public class DelayedHandler extends Handler.Wrapper return switch (mimeType) { case FORM_ENCODED -> new UntilFormDelayedProcess(handler, request, response, callback, contentType); - case MULTIPART_FORM_DATA -> new UntilMultiPartDelayedProcess(handler, request, response, callback, contentType); default -> new UntilContentDelayedProcess(handler, request, response, callback); }; } @@ -270,102 +266,4 @@ public class DelayedHandler extends Handler.Wrapper Response.writeError(getRequest(), getResponse(), getCallback(), x); } } - - protected static class UntilMultiPartDelayedProcess extends DelayedProcess - { - private final MultiPartFormData _formData; - - public UntilMultiPartDelayedProcess(Handler handler, Request wrapped, Response response, Callback callback, String contentType) - { - super(handler, wrapped, response, callback); - String boundary = MultiPart.extractBoundary(contentType); - _formData = boundary == null ? null : new MultiPartFormData(boundary); - } - - private void process(MultiPartFormData.Parts parts, Throwable x) - { - if (x == null) - { - getRequest().setAttribute(MultiPartFormData.Parts.class.getName(), parts); - super.process(); - } - else - { - Response.writeError(getRequest(), getResponse(), getCallback(), x); - } - } - - private void executeProcess(MultiPartFormData.Parts parts, Throwable x) - { - if (x == null) - { - // We must execute here as even though we have consumed all the input, we are probably - // invoked in a demand runnable that is serialized with any write callbacks that might be done in process - getRequest().getContext().execute(() -> process(parts, x)); - } - else - { - Response.writeError(getRequest(), getResponse(), getCallback(), x); - } - } - - @Override - public void delay() - { - if (_formData == null) - { - this.process(); - } - else - { - _formData.setFilesDirectory(getRequest().getContext().getTempDirectory().toPath()); - readAndParse(); - // if we are done already, then we are still in the scope of the original process call and can - // process directly, otherwise we must execute a call to process as we are within a serialized - // demand callback. - if (_formData.isDone()) - { - try - { - MultiPartFormData.Parts parts = _formData.join(); - process(parts, null); - } - catch (Throwable t) - { - process(null, t); - } - } - else - { - _formData.whenComplete(this::executeProcess); - } - } - } - - private void readAndParse() - { - while (!_formData.isDone()) - { - Content.Chunk chunk = getRequest().read(); - if (chunk == null) - { - getRequest().demand(this::readAndParse); - return; - } - if (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 00000000000..cfd0b1ec650 --- /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 6e787edc799..745de4c3ada 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 class MultiPartByteRangesTest assertNotNull(contentType); String boundary = MultiPart.extractBoundary(contentType); - MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary); - byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()))); - MultiPartByteRanges.Parts parts = byteRanges.join(); + MultiPartByteRanges.Parser byteRanges = new MultiPartByteRanges.Parser(boundary); + MultiPartByteRanges.Parts parts = byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()))).join(); assertEquals(3, parts.size()); MultiPart.Part part1 = parts.get(0); 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 d46555dd6c4..e9087a0a8ad 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.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.file.Path; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -44,7 +42,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -79,7 +76,8 @@ public class MultiPartFormDataHandlerTest public boolean handle(Request request, Response response, Callback callback) { String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE)); - new MultiPartFormData(boundary).parse(request) + new MultiPartFormData.Parser(boundary) + .parse(request) .whenComplete((parts, failure) -> { if (parts != null) @@ -118,72 +116,6 @@ public class MultiPartFormDataHandlerTest } } - @Test - public void testDelayedUntilFormData() throws Exception - { - DelayedHandler delayedHandler = new DelayedHandler(); - CountDownLatch processLatch = new CountDownLatch(1); - delayedHandler.setHandler(new Handler.Abstract.NonBlocking() - { - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - processLatch.countDown(); - MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName()); - assertNotNull(parts); - MultiPart.Part part = parts.get(0); - Content.copy(part.getContentSource(), response, callback); - return true; - } - }); - start(delayedHandler); - - try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) - { - String contentBegin = """ - --A1B2C3 - Content-Disposition: form-data; name="part" - - """; - String contentMiddle = """ - 0123456789\ - """; - String contentEnd = """ - ABCDEF - --A1B2C3-- - """; - String header = """ - POST / HTTP/1.1 - Host: localhost - Content-Type: multipart/form-data; boundary=A1B2C3 - Content-Length: $L - - """.replace("$L", String.valueOf(contentBegin.length() + contentMiddle.length() + contentEnd.length())); - - client.write(UTF_8.encode(header)); - client.write(UTF_8.encode(contentBegin)); - - // Verify that the handler has not been called yet. - assertFalse(processLatch.await(1, TimeUnit.SECONDS)); - - client.write(UTF_8.encode(contentMiddle)); - - // Verify that the handler has not been called yet. - assertFalse(processLatch.await(1, TimeUnit.SECONDS)); - - // Finish to send the content. - client.write(UTF_8.encode(contentEnd)); - - // Verify that the handler has been called. - assertTrue(processLatch.await(5, TimeUnit.SECONDS)); - - HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); - assertNotNull(response); - assertEquals(HttpStatus.OK_200, response.getStatus()); - assertEquals("0123456789ABCDEF", response.getContent()); - } - } - @Test public void testEchoMultiPart() throws Exception { @@ -193,13 +125,15 @@ public class MultiPartFormDataHandlerTest public boolean handle(Request request, Response response, Callback callback) { String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE)); - new MultiPartFormData(boundary).parse(request) + + new MultiPartFormData.Parser(boundary) + .parse(request) .whenComplete((parts, failure) -> { if (parts != null) { - response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(parts.getMultiPartFormData().getBoundary())); - MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(parts.getMultiPartFormData().getBoundary()); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(boundary)); + MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(boundary); source.setPartHeadersMaxLength(1024); parts.forEach(source::addPart); source.close(); @@ -310,22 +244,22 @@ public class MultiPartFormDataHandlerTest String boundary = MultiPart.extractBoundary(value); assertNotNull(boundary); - MultiPartFormData formData = new MultiPartFormData(boundary); + ByteBufferContentSource byteBufferContentSource = new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary); formData.setFilesDirectory(tempDir); - formData.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()))); - MultiPartFormData.Parts parts = formData.join(); - - assertEquals(2, parts.size()); - MultiPart.Part part1 = parts.get(0); - assertEquals("part1", part1.getName()); - assertEquals("hello", part1.getContentAsString(UTF_8)); - MultiPart.Part part2 = parts.get(1); - assertEquals("part2", part2.getName()); - assertEquals("file2.bin", part2.getFileName()); - HttpFields headers2 = part2.getHeaders(); - assertEquals(2, headers2.size()); - assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE)); - assertEquals(32, part2.getContentSource().getLength()); + try (MultiPartFormData.Parts parts = formData.parse(byteBufferContentSource).join()) + { + assertEquals(2, parts.size()); + MultiPart.Part part1 = parts.get(0); + assertEquals("part1", part1.getName()); + assertEquals("hello", part1.getContentAsString(UTF_8)); + MultiPart.Part part2 = parts.get(1); + assertEquals("part2", part2.getName()); + assertEquals("file2.bin", part2.getFileName()); + HttpFields headers2 = part2.getHeaders(); + assertEquals(2, headers2.size()); + assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE)); + } } } } 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 f335ace1f6e..70b07e1f5fc 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 @@ public class ResourceHandlerByteRangesTest String contentType = response.get(HttpHeader.CONTENT_TYPE); assertThat(contentType, startsWith(responseContentType)); String boundary = MultiPart.extractBoundary(contentType); - MultiPartByteRanges.Parts parts = new MultiPartByteRanges(boundary) + MultiPartByteRanges.Parts parts = new MultiPartByteRanges.Parser(boundary) .parse(new ByteBufferContentSource(response.getContentByteBuffer())) .join(); assertEquals(2, parts.size()); 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 f7b9c918df9..1c94cd813cb 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 class GzipHandlerTest public boolean handle(Request request, Response response, Callback callback) throws Exception { response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); - Fields queryParameters = Request.extractQueryParameters(request); - FormFields futureFormFields = new FormFields(request, StandardCharsets.UTF_8, -1, -1); - futureFormFields.run(); - Fields formParameters = futureFormFields.get(); + Fields formParameters = FormFields.from(request, UTF_8, -1, -1).get(); Fields parameters = Fields.combine(queryParameters, formParameters); String dump = parameters.stream().map(f -> "%s: %s\n".formatted(f.getName(), f.getValue())).collect(Collectors.joining()); 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 14bd1ce54f6..f99ebd5ad82 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 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration } //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 d1bc1702c1f..4cf48e546e7 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 class Dispatcher implements RequestDispatcher { 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 00000000000..015d051edda --- /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 d1e1d1ab8ad..9dbf56b1265 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.List; 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 class ServletApiRequest implements HttpServletRequest { 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"); - - ServletContextHandler contextHandler = getServletRequestInfo().getServletContext().getServletContextHandler(); - int maxFormContentSize = contextHandler.getMaxFormContentSize(); - int maxFormKeys = contextHandler.getMaxFormKeys(); - - _parts = ServletMultiPartFormData.from(this, maxFormKeys); - Collection parts = _parts.getParts(); - - String formCharset = null; - Part charsetPart = _parts.getPart("_charset_"); - if (charsetPart != null) + try { - try (InputStream is = charsetPart.getInputStream()) - { - formCharset = IO.toString(is, StandardCharsets.UTF_8); - } - } + CompletableFuture futureServletMultiPartFormData = ServletMultiPartFormData.from(this); - /* - Select Charset to use for this part. (NOTE: charset behavior is for the part value only and not the part header/field names) + _parts = futureServletMultiPartFormData.get(); + + Collection parts = _parts.getParts(); + + String formCharset = null; + Part charsetPart = _parts.getPart("_charset_"); + if (charsetPart != null) + { + 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) 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 class ServletApiRequest implements HttpServletRequest (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) + // 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(); + + long formContentSize = 0; + int count = 0; + for (Part p : parts) { - formContentSize = Math.addExact(formContentSize, p.getSize()); - if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize) - throw new IllegalStateException("Form is larger than max length " + maxFormContentSize); + if (maxFormKeys > 0 && ++count > maxFormKeys) + throw new IllegalStateException("Too many form keys > " + maxFormKeys); - // 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()) + 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 class ServletApiRequest implements HttpServletRequest 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 @@ public class ServletApiRequest implements HttpServletRequest { 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 b09fc410651..17da4f12ff6 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.HttpFields; 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 class ServletContextRequest extends ContextRequest implements ServletCont 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 class ServletContextRequest extends ContextRequest implements ServletCont 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 a3079dc50cc..a290c7d8d8b 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 class ServletHolder extends Holder implements Comparable implements Comparable implements Comparable addMapping(String... urlPatterns) @@ -994,12 +991,12 @@ public class ServletHolder extends Holder implements Comparable implements ComparableServlet 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 @@ import org.eclipse.jetty.util.StringUtil; 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"); + // 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")); - String boundary = MultiPart.extractBoundary(request.getContentType()); - if (boundary == null) - throw new IllegalStateException("No multipart boundary parameter in 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")); - // Store MultiPartFormData as attribute on request so it is released by the HttpChannel. - MultiPartFormData formData = new MultiPartFormData(boundary); - formData.setMaxParts(maxParts); + // 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")); - 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); + // Get a temporary directory for larger parts. + File filesDirectory = StringUtil.isBlank(config.getLocation()) + ? servletContextRequest.getContext().getTempDirectory() + : new File(config.getLocation()); - 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) + // 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)); + + // cache the result in attributes. + servletRequest.setAttribute(ServletMultiPartFormData.class.getName(), futureServletParts); } - - Parts parts = new Parts(formData.join()); - request.setAttribute(Parts.class.getName(), parts); - return parts; + return futureServletParts; } /** @@ -158,9 +156,9 @@ public class ServletMultiPartFormData { 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 class ServletMultiPartFormData 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 class ServletMultiPartFormData 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 2ccbbde45b9..9e46e56f678 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.IOException; 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.HttpStatus; 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.IO; 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 class MultiPartServletTest 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 class MultiPartServletTest 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 @@ public class MultiPartServletTest 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 @@ public class MultiPartServletTest 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 @@ public class MultiPartServletTest { 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 @@ public class MultiPartServletTest 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 @@ public class MultiPartServletTest 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 @@ public class MultiPartServletTest } } - @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 @@ public class MultiPartServletTest .body(multiPart) .send(); - assertEquals(500, response.getStatus()); + assertEquals(400, response.getStatus()); assertThat(response.getContentAsString(), containsString("max file size exceeded")); } @@ -370,18 +371,34 @@ public class MultiPartServletTest 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 @@ public class MultiPartServletTest 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 @@ public class MultiPartServletTest 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 @@ public class MultiPartServletTest "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 @@ public class MultiPartServletTest 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 0e4e9f8d2b0..75a8ec27097 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 class StandardDescriptorProcessor extends IterativeDescriptorProcessor 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());