diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index 4ea70faa974..98e009121fc 100644 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -10,6 +10,9 @@ labels: Bug **Jetty version(s)** +**Jetty Environment** + + **Java version/vendor** `(use: java -version)` **OS type/version** diff --git a/.github/ISSUE_TEMPLATE/question-template.md b/.github/ISSUE_TEMPLATE/question-template.md index d9cb028ca8b..a36ca9e1bbe 100644 --- a/.github/ISSUE_TEMPLATE/question-template.md +++ b/.github/ISSUE_TEMPLATE/question-template.md @@ -7,9 +7,12 @@ assignees: '' --- -**Jetty version** +**Jetty Version** -**Java version** +**Jetty Environment** + + +**Java Version** **Question** diff --git a/.github/ISSUE_TEMPLATE/release-template.md b/.github/ISSUE_TEMPLATE/release-template.md index feb328263f6..29e0a76e164 100644 --- a/.github/ISSUE_TEMPLATE/release-template.md +++ b/.github/ISSUE_TEMPLATE/release-template.md @@ -55,7 +55,7 @@ This release process will produce releases: - [ ] Promote staged releases. - [ ] Merge release branches back to main branches and delete release branches. - [ ] Verify release existence in Maven Central by triggering the Jenkins builds of CometD. -- [ ] Update Jetty versions on the web sites. +- [ ] Update Jetty versions on the website ( follow instructions in [jetty-website](https://github.com/eclipse/jetty-website/blob/master/README.md) ). + [ ] Update (or check) [Download](https://eclipse.dev/jetty/download.php) page is updated. + [ ] Update (or check) documentation page(s) are updated. - [ ] Publish GitHub Releases in the order of oldest (eg: 9) to newest (eg: 11) (to ensure that "latest" in github is truly the latest) diff --git a/Jenkinsfile-autobahn b/Jenkinsfile-autobahn index 374830be9e8..5ea442588b9 100644 --- a/Jenkinsfile-autobahn +++ b/Jenkinsfile-autobahn @@ -18,8 +18,14 @@ pipeline { } steps { timeout( time: 120, unit: 'MINUTES' ) { - mavenBuild( "jdk11", "-T3 clean install -Djacoco.skip=true -Pautobahn", "maven3", true ) // + mavenBuild( "jdk11", "-T3 clean install -Djacoco.skip=true -pl :test-websocket-autobahn -am -Pautobahn -Dtest=AutobahnTests", "maven3" ) // junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml,**/target/autobahntestsuite-reports/*.xml' + publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/core/servers", reportFiles: 'index.html', reportName: 'Autobahn Report Core Server', reportTitles: '']) + publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/core/clients", reportFiles: 'index.html', reportName: 'Autobahn Report Core Client', reportTitles: '']) + publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/javax/servers", reportFiles: 'index.html', reportName: 'Autobahn Report Javax Server', reportTitles: '']) + publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/javax/clients", reportFiles: 'index.html', reportName: 'Autobahn Report Javax Client', reportTitles: '']) + publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/jetty/servers", reportFiles: 'index.html', reportName: 'Autobahn Report Jetty Server', reportTitles: '']) + publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/jetty/clients", reportFiles: 'index.html', reportName: 'Autobahn Report Jetty Client', reportTitles: '']) } } } diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc index 325b03dcf22..32972a382ad 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc @@ -97,8 +97,52 @@ [[pg-migration-11-to-12-servlet-to-handler]] ==== Migrate Servlets to Jetty Handlers +Web applications written using the Servlet APIs may be re-written using the Jetty `Handler` APIs. +The sections below outline the Jetty `Handler` APIs that correspond to the Servlet APIs. +To replace the functionalities of Servlet Filters, refer to xref:pg-server-http-handler[this section]. +===== Handler Request APIs +[source,java,indent=0] +---- +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=request] +---- + +===== Handler Request Content APIs +[source,java,indent=0] +---- +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-string] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-buffer] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-stream] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-source] +---- + +===== Handler Response APIs +[source,java,indent=0] +---- +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=response] +---- + +===== Handler Response Content APIs +[source,java,indent=0] +---- +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit-status] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-explicit] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-content] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-string] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-echo] + +include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-trailers] +---- [[pg-migration-11-to-12-api-changes]] ==== APIs Changes 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 5abb2c3aeaf..1dea2d3b092 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 @@ -325,20 +325,28 @@ Jetty will send an HTTP `404` response anyway if `DefaultHandler` is not used. `ServletContextHandler` is a `ContextHandler` that provides support for the Servlet APIs and implements the behaviors required by the Servlet specification. -The Maven artifact coordinates are: +The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: [source,xml,subs=normal] ---- - org.eclipse.jetty - jetty-servlet + org.eclipse.jetty.{ee-all} + jetty-{ee-all}-servlet {version} ---- +For example, for Jakarta {ee-current-caps} the coordinates are: `org.eclipse.jetty.ee10:jetty-ee10-servlet:{version}`. + +Below you can find an example of how to setup a Jakarta {ee-current-caps} `ServletContextHandler`: + [source,java,indent=0] ---- -include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler] +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler-servlet] +---- +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler-setup] ---- The `Handler` and Servlet components tree structure looks like the following: @@ -368,6 +376,17 @@ Server applications must be careful when creating the `Handler` tree to put ``Se `WebAppContext` is a `ServletContextHandler` that auto configures itself by reading a `web.xml` Servlet configuration file. +The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: + +[source,xml,subs=normal] +---- + + org.eclipse.jetty.{ee-all} + jetty-{ee-all}-webapp + {version} + +---- + Server applications can specify a `+*.war+` file or a directory with the structure of a `+*.war+` file to `WebAppContext` to deploy a standard Servlet web application packaged as a `war` (as defined by the Servlet specification). Where server applications using `ServletContextHandler` must manually invoke methods to add ``Servlet``s and ``Filter``s, `WebAppContext` reads `WEB-INF/web.xml` to add ``Servlet``s and ``Filter``s, and also enforces a number of restrictions defined by the Servlet specification, in particular related to class loading. @@ -412,6 +431,19 @@ However, Jetty picks good defaults and allows server applications to customize _ If you have a xref:pg-server-http-handler-use-servlet-context[Servlet web application], you may want to use a `DefaultServlet` instead of `ResourceHandler`. The features are similar, but `DefaultServlet` is more commonly used to serve static files for Servlet web applications. +The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: + +[source,xml,subs=normal] +---- + + org.eclipse.jetty.{ee-all} + jetty-{ee-all}-servlet + {version} + +---- + +Below you can find an example of how to setup `DefaultServlet`: + [source,java,indent=0] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=defaultServlet] diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java new file mode 100644 index 00000000000..4e71ee37907 --- /dev/null +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java @@ -0,0 +1,643 @@ +// +// ======================================================================== +// 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.migration; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.Trailers; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Context; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Session; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.CompletableTask; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.Promise; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@SuppressWarnings("unused") +public class ServletToHandlerDocs +{ + @SuppressWarnings("InnerClassMayBeStatic") + // tag::request[] + public class RequestAPIs extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Gets the request method. + // Replaces: + // - servletRequest.getMethod(); + String method = request.getMethod(); + + // Gets the request protocol name and version. + // Replaces: + // - servletRequest.getProtocol(); + String protocol = request.getConnectionMetaData().getProtocol(); + + // Gets the full request URI. + // Replaces: + // - servletRequest.getRequestURL(); + String fullRequestURI = request.getHttpURI().asString(); + + // Gets the request context. + // Replaces: + // - servletRequest.getServletContext() + Context context = request.getContext(); + + // Gets the context path. + // Replaces: + // - servletRequest.getContextPath() + String contextPath = context.getContextPath(); + + // Gets the request path. + // Replaces: + // - servletRequest.getRequestURI(); + String requestPath = request.getHttpURI().getPath(); + + // Gets the request path after the context path. + // Replaces: + // - servletRequest.getServletPath() + servletRequest.getPathInfo() + String pathInContext = Request.getPathInContext(request); + + // Gets the request query. + // Replaces: + // - servletRequest.getQueryString() + String queryString = request.getHttpURI().getQuery(); + + // Gets request parameters. + // Replaces: + // - servletRequest.getParameterNames(); + // - servletRequest.getParameter(name); + // - servletRequest.getParameterValues(name); + // - servletRequest.getParameterMap(); + Fields queryParameters = Request.extractQueryParameters(request, UTF_8); + Fields allParameters = Request.getParameters(request); + + // Gets cookies. + // Replaces: + // - servletRequest.getCookies(); + List cookies = Request.getCookies(request); + + // Gets request HTTP headers. + // Replaces: + // - servletRequest.getHeaderNames() + // - servletRequest.getHeader(name) + // - servletRequest.getHeaders(name) + // - servletRequest.getDateHeader(name) + // - servletRequest.getIntHeader(name) + HttpFields requestHeaders = request.getHeaders(); + + // Gets the request Content-Type. + // Replaces: + // - servletRequest.getContentType() + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + + // Gets the request Content-Length. + // Replaces: + // - servletRequest.getContentLength() + // - servletRequest.getContentLengthLong() + long contentLength = request.getHeaders().getLongField(HttpHeader.CONTENT_LENGTH); + + // Gets the request locales. + // Replaces: + // - servletRequest.getLocale() + // - servletRequest.getLocales() + List locales = Request.getLocales(request); + + // Gets the request scheme. + // Replaces: + // - servletRequest.getScheme() + String scheme = request.getHttpURI().getScheme(); + + // Gets the server name. + // Replaces: + // - servletRequest.getServerName() + String serverName = Request.getServerName(request); + + // Gets the server port. + // Replaces: + // - servletRequest.getServerPort() + int serverPort = Request.getServerPort(request); + + // Gets the remote host/address. + // Replaces: + // - servletRequest.getRemoteAddr() + // - servletRequest.getRemoteHost() + String remoteAddress = Request.getRemoteAddr(request); + + // Gets the remote port. + // Replaces: + // - servletRequest.getRemotePort() + int remotePort = Request.getRemotePort(request); + + // Gets the local host/address. + // Replaces: + // - servletRequest.getLocalAddr() + // - servletRequest.getLocalHost() + String localAddress = Request.getLocalAddr(request); + + // Gets the local port. + // Replaces: + // - servletRequest.getLocalPort() + int localPort = Request.getLocalPort(request); + + // Gets the request attributes. + // Replaces: + // - servletRequest.getAttributeNames() + // - servletRequest.getAttribute(name) + // - servletRequest.setAttribute(name, value) + // - servletRequest.removeAttribute(name) + String name = "name"; + Object value = "value"; + Set names = request.getAttributeNameSet(); + Object attribute = request.getAttribute(name); + request.setAttribute(name, value); + request.removeAttribute(name); + request.clearAttributes(); + Map map = request.asAttributeMap(); + + // Gets the request trailers. + // Replaces: + // - servletRequest.getTrailerFields() + HttpFields trailers = request.getTrailers(); + + // Gets the HTTP session. + // Replaces: + // - servletRequest.getSession() + // - servletRequest.getSession(create) + boolean create = true; + Session session = request.getSession(create); + + callback.succeeded(); + return false; + } + } + // end::request[] + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsString extends Handler.Abstract + { + // tag::requestContent-string[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Non-blocking read the request content as a String. + // Use with caution as the request content may be large. + Promise.Completable completable = new Promise.Completable<>(); + Content.Source.asString(request, UTF_8, completable); + + completable.whenComplete((requestContent, failure) -> + { + if (failure == null) + { + // Process the request content here. + + // Implicitly respond with status code 200 and no content. + callback.succeeded(); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::requestContent-string[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsByteBuffer extends Handler.Abstract + { + // tag::requestContent-buffer[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Non-blocking read the request content as a ByteBuffer. + // Use with caution as the request content may be large. + Promise.Completable completable = new Promise.Completable<>(); + Content.Source.asByteBuffer(request, completable); + + completable.whenComplete((requestContent, failure) -> + { + if (failure == null) + { + // Process the request content here. + + // Implicitly respond with status code 200 and no content. + callback.succeeded(); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::requestContent-buffer[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsInputStream extends Handler.Abstract + { + // tag::requestContent-stream[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Read the request content as an InputStream. + // Note that InputStream.read() may block. + try (InputStream inputStream = Content.Source.asInputStream(request)) + { + while (true) + { + int read = inputStream.read(); + + // EOF was reached, stop reading. + if (read < 0) + break; + + // Process the read byte here. + } + } + + // Implicitly respond with status code 200 and no content. + callback.succeeded(); + return true; + } + // end::requestContent-stream[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsSource extends Handler.Abstract + { + // tag::requestContent-source[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + CompletableTask reader = new CompletableTask<>() + { + @Override + public void run() + { + // Read in a loop. + while (true) + { + // Read a chunk of content. + Content.Chunk chunk = request.read(); + + // If there is no content, demand to be + // called back when more content is available. + if (chunk == null) + { + request.demand(this); + return; + } + + // If a failure is read, complete with a failure. + if (Content.Chunk.isFailure(chunk)) + { + Throwable failure = chunk.getFailure(); + completeExceptionally(failure); + return; + } + + if (chunk instanceof Trailers trailers) + { + // Possibly process the request trailers here. + // Trailers have an empty ByteBuffer and are a last chunk. + } + + // Process the request content chunk here. + // After the processing, the chunk MUST be released. + chunk.release(); + + // If the last chunk is read, complete normally. + if (chunk.isLast()) + { + complete(null); + return; + } + + // Not the last chunk of content, loop around to read more. + } + } + }; + + // Initiate the read of the request content. + reader.start(); + + // When the read is complete, complete the Handler callback. + callback.completeWith(reader); + + return true; + } + // end::requestContent-source[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::response[] + public class ResponseAPIs extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Sets/Gets the response HTTP status. + // Replaces: + // - servletResponse.setStatus(code); + // - servletResponse.getStatus(); + response.setStatus(HttpStatus.OK_200); + int status = response.getStatus(); + + // Gets the response HTTP headers. + // Replaces: + // - servletResponse.setHeader(name, value); + // - servletResponse.addHeader(name, value); + // - servletResponse.setDateHeader(name, date); + // - servletResponse.addDateHeader(name, date); + // - servletResponse.setIntHeader(name, value); + // - servletResponse.addIntHeader(name, value); + // - servletResponse.getHeaderNames() + // - servletResponse.getHeader(name) + // - servletResponse.getHeaders(name) + // - servletResponse.containsHeader(name) + HttpFields.Mutable responseHeaders = response.getHeaders(); + + // Sets an HTTP cookie. + // Replaces: + // - servletResponse.addCookie(cookie); + HttpCookie cookie = HttpCookie.build("name", "value") + .domain("example.org") + .path("/path") + .maxAge(Duration.ofDays(1).toSeconds()) + .build(); + Response.addCookie(response, cookie); + + // Sets the response Content-Type. + // Replaces: + // - servletResponse.setContentType(type) + responseHeaders.put(HttpHeader.CONTENT_TYPE, "text/plain; charset=UTF-8"); + + // Sets the response Content-Length. + // Replaces: + // - servletResponse.setContentLength(length) + // - servletResponse.setContentLengthLong(length) + responseHeaders.put(HttpHeader.CONTENT_LENGTH, 1024L); + + // Sets/Gets the response trailers. + // Replaces: + // - servletResponse.setTrailerFields(() -> trailers) + // - servletResponse.getTrailerFields() + HttpFields trailers = HttpFields.build().put("checksum", 0xCAFE); + response.setTrailersSupplier(trailers); + Supplier trailersSupplier = response.getTrailersSupplier(); + + // Gets whether the response is committed. + // Replaces: + // - servletResponse.isCommitted() + boolean committed = response.isCommitted(); + + // Resets the response. + // Replaces: + // - servletResponse.reset(); + response.reset(); + + // Sends a redirect response. + // Replaces: + // - servletResponse.encodeRedirectURL(location) + // - servletResponse.sendRedirect(location) + String location = Request.toRedirectURI(request, "/redirect"); + Response.sendRedirect(request, response, callback, location); + + // Sends an error response. + // Replaces: + // - servletResponse.sendError(code); + // - servletResponse.sendError(code, message); + Response.writeError(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, "Request Cannot be Processed"); + + callback.succeeded(); + return true; + } + } + // end::response[] + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIsImplicit extends Handler.Abstract + { + // tag::responseContent-implicit[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Produces an implicit response with status code 200 + // with no content when returning from this method. + + // The Handler callback must be completed when returning true. + callback.succeeded(); + return true; + } + // end::responseContent-implicit[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIsImplicitWithStatus extends Handler.Abstract + { + // tag::responseContent-implicit-status[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Produces an implicit response with status 204 + // with no content when returning from this method. + response.setStatus(HttpStatus.NO_CONTENT_204); + + // The Handler callback must be completed when returning true. + callback.succeeded(); + return true; + } + // end::responseContent-implicit-status[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIsExplicit extends Handler.Abstract + { + // tag::responseContent-explicit[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Produces an explicit response with status 204 with no content. + response.setStatus(HttpStatus.NO_CONTENT_204); + + // This explicit first write() writes the response status code and headers. + // It is also the last write (as specified by the first parameter) + // and writes an empty content (the second parameter, a null ByteBuffer). + // When this write completes, the Handler callback is completed. + response.write(true, null, callback); + + return true; + } + // end::responseContent-explicit[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPISimpleContent extends Handler.Abstract + { + // tag::responseContent-content[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + ByteBuffer content = UTF_8.encode("Hello World"); + + // Explicit first write that writes the response status code, headers and content. + // When this write completes, the Handler callback is completed. + response.write(true, content, callback); + + return true; + } + // end::responseContent-content[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIFlush extends Handler.Abstract + { + // tag::responseContent-content[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + ByteBuffer content = UTF_8.encode("Hello World"); + response.getHeaders().put(HttpHeader.CONTENT_LENGTH, content.remaining()); + + // Flush the response status code and the headers (no content). + // This is the fist but non-last write. + Callback.Completable completable = new Callback.Completable(); + response.write(false, null, completable); + + // When the first write completes, perform the second (and last) write. + completable.whenComplete((ignored, failure) -> + { + if (failure == null) + { + // Now explicitly write the content as the last write. + // When this write completes, the Handler callback is completed. + response.write(true, content, callback); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::responseContent-content[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIString extends Handler.Abstract + { + // tag::responseContent-string[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + // Utility method to write UTF-8 string content. + // When this write completes, the Handler callback is completed. + Content.Sink.write(response, true, "Hello World", callback); + + return true; + } + // end::responseContent-string[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIEcho extends Handler.Abstract + { + // tag::responseContent-echo[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + // Utility method to echo the content from the request to the response. + // When the echo completes, the Handler callback is completed. + Content.copy(request, response, callback); + + return true; + } + // end::responseContent-echo[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPITrailers extends Handler.Abstract + { + // tag::responseContent-trailers[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + // The trailers must be set on the response before the first write. + HttpFields.Mutable trailers = HttpFields.build(); + response.setTrailersSupplier(trailers); + + // Explicit first write that writes the response status code, headers and content. + // The trailers have not been written yet; they will be written with the last write. + ByteBuffer content = UTF_8.encode("Hello World"); + Callback.Completable completable = new Callback.Completable(); + response.write(false, content, completable); + + completable.whenComplete((ignored, failure) -> + { + if (failure == null) + { + // Update the trailers + trailers.put("Content-Checksum", 0xCAFE); + + // Explicit last write to write the trailers + // and complete the Handler callback. + response.write(true, null, callback); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::responseContent-trailers[] + } +} 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 07a0cdcc664..347e15221d4 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 @@ -658,18 +658,21 @@ public class HTTPServerDocs // end::contextHandlerCollection[] } + @SuppressWarnings("InnerClassMayBeStatic") + // tag::servletContextHandler-servlet[] + public class ShopCartServlet extends HttpServlet + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + // Implement the shop cart functionality. + } + } + // end::servletContextHandler-servlet[] + public void servletContextHandler() throws Exception { - // tag::servletContextHandler[] - class ShopCartServlet extends HttpServlet - { - @Override - protected void service(HttpServletRequest request, HttpServletResponse response) - { - // Implement the shop cart functionality. - } - } - + // tag::servletContextHandler-setup[] Server server = new Server(); Connector connector = new ServerConnector(server); server.addConnector(connector); @@ -692,7 +695,7 @@ public class HTTPServerDocs server.setHandler(context); server.start(); - // end::servletContextHandler[] + // end::servletContextHandler-setup[] } public void webAppContextHandler() throws Exception diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java index 89928e257df..30a995f3a3b 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.client; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import org.eclipse.jetty.client.transport.HttpDestination; @@ -23,6 +24,7 @@ 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.io.ArrayByteBufferPool; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Handler; @@ -33,6 +35,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.Callback; +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; @@ -46,15 +49,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class HttpClientProxyProtocolTest { + private ArrayByteBufferPool.Tracking serverBufferPool; private Server server; private ServerConnector connector; + private ArrayByteBufferPool.Tracking clientBufferPool; private HttpClient client; private void startServer(Handler handler) throws Exception { QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); - server = new Server(serverThreads); + serverBufferPool = new ArrayByteBufferPool.Tracking(); + server = new Server(serverThreads, null, serverBufferPool); HttpConnectionFactory http = new HttpConnectionFactory(); ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol()); connector = new ServerConnector(server, 1, 1, proxy, http); @@ -67,18 +73,22 @@ public class HttpClientProxyProtocolTest { QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); + clientBufferPool = new ArrayByteBufferPool.Tracking(); client = new HttpClient(); client.setExecutor(clientThreads); + client.setByteBufferPool(clientBufferPool); client.start(); } @AfterEach public void dispose() throws Exception { - if (server != null) - server.stop(); - if (client != null) - client.stop(); + LifeCycle.stop(client); + LifeCycle.stop(server); + Set serverLeaks = serverBufferPool.getLeaks(); + assertEquals(0, serverLeaks.size(), serverBufferPool.dumpLeaks()); + Set clientLeaks = clientBufferPool.getLeaks(); + assertEquals(0, clientLeaks.size(), clientBufferPool.dumpLeaks()); } @Test diff --git a/jetty-core/jetty-client/src/test/resources/jetty-logging.properties b/jetty-core/jetty-client/src/test/resources/jetty-logging.properties index e194e90d87c..90423896e45 100644 --- a/jetty-core/jetty-client/src/test/resources/jetty-logging.properties +++ b/jetty-core/jetty-client/src/test/resources/jetty-logging.properties @@ -1,5 +1,6 @@ #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG +#org.eclipse.jetty.io.ArrayByteBufferPool$Tracking.LEVEL=DEBUG #org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.io.ssl.LEVEL=DEBUG #org.eclipse.jetty.http.LEVEL=DEBUG diff --git a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java index 0d6bc46b254..0c3757e55a2 100644 --- a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java +++ b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java @@ -197,8 +197,7 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements } } - if (LOG.isDebugEnabled()) - LOG.debug("{} ignored {}", this, app); + LOG.warn("{} no environment for {}, ignoring", this, app); return null; } diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpChannelOverFCGI.java b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpChannelOverFCGI.java index 4a80780ca81..27678000d57 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpChannelOverFCGI.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpChannelOverFCGI.java @@ -13,45 +13,33 @@ package org.eclipse.jetty.fcgi.client.transport.internal; -import java.util.concurrent.TimeoutException; - import org.eclipse.jetty.client.Result; import org.eclipse.jetty.client.transport.HttpChannel; import org.eclipse.jetty.client.transport.HttpExchange; import org.eclipse.jetty.client.transport.HttpReceiver; import org.eclipse.jetty.client.transport.HttpSender; -import org.eclipse.jetty.fcgi.generator.Flusher; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.IdleTimeout; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class HttpChannelOverFCGI extends HttpChannel { - private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverFCGI.class); - private final HttpConnectionOverFCGI connection; - private final Flusher flusher; private final HttpSenderOverFCGI sender; private final HttpReceiverOverFCGI receiver; - private final FCGIIdleTimeout idle; private int request; private HttpVersion version; - public HttpChannelOverFCGI(HttpConnectionOverFCGI connection, Flusher flusher, long idleTimeout) + public HttpChannelOverFCGI(HttpConnectionOverFCGI connection) { super(connection.getHttpDestination()); this.connection = connection; - this.flusher = flusher; this.sender = new HttpSenderOverFCGI(this); this.receiver = new HttpReceiverOverFCGI(this); - this.idle = new FCGIIdleTimeout(connection, idleTimeout); } public HttpConnectionOverFCGI getHttpConnection() @@ -81,28 +69,21 @@ public class HttpChannelOverFCGI extends HttpChannel return receiver; } - public boolean isFailed() - { - return sender.isFailed() || receiver.isFailed(); - } - @Override public void send(HttpExchange exchange) { version = exchange.getRequest().getVersion(); - idle.onOpen(); sender.send(exchange); } @Override public void release() { - connection.release(this); + connection.release(); } protected void responseBegin(int code, String reason) { - idle.notIdle(); HttpExchange exchange = getHttpExchange(); if (exchange == null) return; @@ -119,7 +100,6 @@ public class HttpChannelOverFCGI extends HttpChannel protected void responseHeaders() { - idle.notIdle(); HttpExchange exchange = getHttpExchange(); if (exchange != null) receiver.responseHeaders(exchange); @@ -127,7 +107,6 @@ public class HttpChannelOverFCGI extends HttpChannel protected void content(Content.Chunk chunk) { - idle.notIdle(); HttpExchange exchange = getHttpExchange(); if (exchange != null) receiver.content(chunk); @@ -135,7 +114,6 @@ public class HttpChannelOverFCGI extends HttpChannel protected void end() { - idle.notIdle(); HttpExchange exchange = getHttpExchange(); if (exchange != null) receiver.end(exchange); @@ -150,67 +128,33 @@ public class HttpChannelOverFCGI extends HttpChannel promise.succeeded(false); } + void eof() + { + HttpExchange exchange = getHttpExchange(); + if (exchange == null) + connection.close(); + } + @Override public void exchangeTerminated(HttpExchange exchange, Result result) { super.exchangeTerminated(exchange, result); - idle.onClose(); HttpFields responseHeaders = result.getResponse().getHeaders(); if (result.isFailed()) connection.close(result.getFailure()); - else if (!connection.closeByHTTP(responseHeaders)) + else if (connection.isShutdown() || connection.isCloseByHTTP(responseHeaders)) + connection.close(); + else release(); } protected void flush(ByteBufferPool.Accumulator accumulator, Callback callback) { - flusher.flush(accumulator, callback); + connection.getFlusher().flush(accumulator, callback); } void receive() { receiver.receive(); } - - private class FCGIIdleTimeout extends IdleTimeout - { - private final HttpConnectionOverFCGI connection; - private boolean open; - - public FCGIIdleTimeout(HttpConnectionOverFCGI connection, long idleTimeout) - { - super(connection.getHttpDestination().getHttpClient().getScheduler()); - this.connection = connection; - setIdleTimeout(idleTimeout >= 0 ? idleTimeout : connection.getEndPoint().getIdleTimeout()); - } - - @Override - public void onOpen() - { - open = true; - notIdle(); - super.onOpen(); - } - - @Override - public void onClose() - { - super.onClose(); - open = false; - } - - @Override - protected void onIdleExpired(TimeoutException timeout) - { - if (LOG.isDebugEnabled()) - LOG.debug("Idle timeout for request {}", request); - connection.abort(timeout); - } - - @Override - public boolean isOpen() - { - return open; - } - } } diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpConnectionOverFCGI.java b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpConnectionOverFCGI.java index 4a28e87880c..7ec4dc64ddd 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpConnectionOverFCGI.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpConnectionOverFCGI.java @@ -13,14 +13,13 @@ package org.eclipse.jetty.fcgi.client.transport.internal; -import java.io.EOFException; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousCloseException; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.client.Connection; import org.eclipse.jetty.client.Destination; @@ -49,7 +48,6 @@ import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.util.Attachable; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Promise; -import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,18 +56,19 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne private static final Logger LOG = LoggerFactory.getLogger(HttpConnectionOverFCGI.class); private final ByteBufferPool networkByteBufferPool; - private final AutoLock lock = new AutoLock(); - private final LinkedList requests = new LinkedList<>(); + private final AtomicInteger requests = new AtomicInteger(); private final AtomicBoolean closed = new AtomicBoolean(); private final HttpDestination destination; private final Promise promise; private final Flusher flusher; private final Delegate delegate; private final ClientParser parser; - private HttpChannelOverFCGI channel; + private final HttpChannelOverFCGI channel; private RetainableByteBuffer networkBuffer; private Object attachment; private Runnable action; + private long idleTimeout; + private boolean shutdown; public HttpConnectionOverFCGI(EndPoint endPoint, Destination destination, Promise promise) { @@ -79,7 +78,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne this.flusher = new Flusher(endPoint); this.delegate = new Delegate(destination); this.parser = new ClientParser(new ResponseListener()); - requests.addLast(0); + this.channel = newHttpChannel(); HttpClient client = destination.getHttpClient(); this.networkByteBufferPool = client.getByteBufferPool(); } @@ -207,13 +206,18 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne private void shutdown() { - // Close explicitly only if we are idle, since the request may still - // be in progress, otherwise close only if we can fail the responses. - HttpChannelOverFCGI channel = this.channel; - if (channel == null || channel.getRequest() == 0) - close(); - else - failAndClose(new EOFException(String.valueOf(getEndPoint()))); + // Mark this receiver as shutdown, so that we can + // close the connection when the exchange terminates. + // We cannot close the connection from here because + // the request may still be in process. + shutdown = true; + if (!parser.eof()) + channel.eof(); + } + + boolean isShutdown() + { + return shutdown; } @Override @@ -226,27 +230,11 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne return false; } - protected void release(HttpChannelOverFCGI channel) + protected void release() { - HttpChannelOverFCGI existing = this.channel; - if (existing == channel) - { - channel.setRequest(0); - // Recycle only non-failed channels. - if (channel.isFailed()) - { - channel.destroy(); - this.channel = null; - } - destination.release(this); - } - else - { - if (existing == null) - channel.destroy(); - else - throw new UnsupportedOperationException("FastCGI Multiplex"); - } + // Restore idle timeout + getEndPoint().setIdleTimeout(idleTimeout); + destination.release(this); } @Override @@ -260,9 +248,8 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne if (closed.compareAndSet(false, true)) { getHttpDestination().remove(this); - abort(failure); - + channel.destroy(); getEndPoint().shutdownOutput(); if (LOG.isDebugEnabled()) LOG.debug("Shutdown {}", this); @@ -290,62 +277,25 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne return attachment; } - protected boolean closeByHTTP(HttpFields fields) + protected boolean isCloseByHTTP(HttpFields fields) { - if (!fields.contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString())) - return false; - close(); - return true; + return fields.contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); } protected void abort(Throwable failure) { - HttpChannelOverFCGI channel = this.channel; - if (channel != null) - { - HttpExchange exchange = channel.getHttpExchange(); - if (exchange != null) - exchange.getRequest().abort(failure); - channel.destroy(); - this.channel = null; - } + HttpExchange exchange = channel.getHttpExchange(); + if (exchange != null) + exchange.getRequest().abort(failure); } private void failAndClose(Throwable failure) { - HttpChannelOverFCGI channel = this.channel; - if (channel != null) + channel.responseFailure(failure, Promise.from(failed -> { - channel.responseFailure(failure, Promise.from(failed -> - { - channel.destroy(); - if (failed) - close(failure); - }, x -> - { - channel.destroy(); + if (failed) close(failure); - })); - } - } - - private int acquireRequest() - { - try (AutoLock ignored = lock.lock()) - { - int last = requests.getLast(); - int request = last + 1; - requests.addLast(request); - return request; - } - } - - private void releaseRequest(int request) - { - try (AutoLock ignored = lock.lock()) - { - requests.removeFirstOccurrence(request); - } + }, x -> close(failure))); } private Runnable getAndSetAction(Runnable action) @@ -355,17 +305,9 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne return r; } - protected HttpChannelOverFCGI acquireHttpChannel(int id, Request request) + protected HttpChannelOverFCGI newHttpChannel() { - if (channel == null) - channel = newHttpChannel(request); - channel.setRequest(id); - return channel; - } - - protected HttpChannelOverFCGI newHttpChannel(Request request) - { - return new HttpChannelOverFCGI(this, getFlusher(), request.getIdleTimeout()); + return new HttpChannelOverFCGI(this); } @Override @@ -388,8 +330,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne @Override protected Iterator getHttpChannels() { - HttpChannel channel = HttpConnectionOverFCGI.this.channel; - return channel == null ? Collections.emptyIterator() : Collections.singleton(channel).iterator(); + return Collections.singleton(channel).iterator(); } @Override @@ -398,9 +339,14 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne HttpRequest request = exchange.getRequest(); normalizeRequest(request); - int id = acquireRequest(); - HttpChannelOverFCGI channel = acquireHttpChannel(id, request); + // Save the old idle timeout to restore it. + EndPoint endPoint = getEndPoint(); + idleTimeout = endPoint.getIdleTimeout(); + long requestIdleTimeout = request.getIdleTimeout(); + if (requestIdleTimeout >= 0) + endPoint.setIdleTimeout(requestIdleTimeout); + channel.setRequest(requests.incrementAndGet()); return send(channel, exchange); } @@ -431,11 +377,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne { if (LOG.isDebugEnabled()) LOG.debug("onBegin r={},c={},reason={}", request, code, reason); - HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel; - if (channel != null) - channel.responseBegin(code, reason); - else - noChannel(request); + channel.responseBegin(code, reason); } @Override @@ -443,11 +385,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne { if (LOG.isDebugEnabled()) LOG.debug("onHeader r={},f={}", request, field); - HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel; - if (channel != null) - channel.responseHeader(field); - else - noChannel(request); + channel.responseHeader(field); } @Override @@ -455,15 +393,9 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne { if (LOG.isDebugEnabled()) LOG.debug("onHeaders r={} {}", request, networkBuffer); - HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel; - if (channel != null) - { - if (getAndSetAction(channel::responseHeaders) != null) - throw new IllegalStateException(); - return true; - } - noChannel(request); - return false; + if (getAndSetAction(channel::responseHeaders) != null) + throw new IllegalStateException(); + return true; } @Override @@ -475,21 +407,13 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne { case STD_OUT -> { - HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel; - if (channel != null) - { - // No need to call networkBuffer.retain() here, since we know - // that the action will be run before releasing the networkBuffer. - // The receiver of the chunk decides whether to consume/retain it. - Content.Chunk chunk = Content.Chunk.asChunk(buffer, false, networkBuffer); - if (getAndSetAction(() -> channel.content(chunk)) == null) - return true; - throw new IllegalStateException(); - } - else - { - noChannel(request); - } + // No need to call networkBuffer.retain() here, since we know + // that the action will be run before releasing the networkBuffer. + // The receiver of the chunk decides whether to consume/retain it. + Content.Chunk chunk = Content.Chunk.asChunk(buffer, false, networkBuffer); + if (getAndSetAction(() -> channel.content(chunk)) == null) + return true; + throw new IllegalStateException(); } case STD_ERR -> LOG.info(BufferUtil.toUTF8String(buffer)); default -> throw new IllegalArgumentException(); @@ -502,16 +426,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne { if (LOG.isDebugEnabled()) LOG.debug("onEnd r={}", request); - HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel; - if (channel != null) - { - releaseRequest(request); - channel.end(); - } - else - { - noChannel(request); - } + channel.end(); } @Override @@ -519,25 +434,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne { if (LOG.isDebugEnabled()) LOG.debug("onFailure request={}", request, failure); - HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel; - if (channel != null) - { - channel.responseFailure(failure, Promise.from(failed -> - { - if (failed) - releaseRequest(request); - }, x -> releaseRequest(request))); - } - else - { - noChannel(request); - } - } - - private void noChannel(int request) - { - if (LOG.isDebugEnabled()) - LOG.debug("Channel not found for request {}", request); + failAndClose(failure); } } } diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpReceiverOverFCGI.java b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpReceiverOverFCGI.java index a59016f108f..b904a8347c1 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpReceiverOverFCGI.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/internal/HttpReceiverOverFCGI.java @@ -61,6 +61,17 @@ public class HttpReceiverOverFCGI extends HttpReceiver } } + @Override + protected void dispose() + { + super.dispose(); + if (chunk != null) + { + chunk.release(); + chunk = null; + } + } + @Override public Content.Chunk read(boolean fillInterestIfNeeded) { diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java index c410c9a9117..a45b17a7deb 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.fcgi.parser; +import java.io.EOFException; import java.nio.ByteBuffer; import org.eclipse.jetty.fcgi.FCGI; @@ -60,7 +61,7 @@ public abstract class Parser protected final HeaderParser headerParser = new HeaderParser(); private final Listener listener; - private State state = State.HEADER; + private State state = State.INITIAL; private int padding; protected Parser(Listener listener) @@ -80,6 +81,12 @@ public abstract class Parser { switch (state) { + case INITIAL -> + { + if (!buffer.hasRemaining()) + return false; + state = State.HEADER; + } case HEADER -> { if (!headerParser.parse(buffer)) @@ -145,10 +152,19 @@ public abstract class Parser protected abstract ContentParser findContentParser(FCGI.FrameType frameType); + public boolean eof() + { + if (state == State.INITIAL) + return false; + Throwable failure = new EOFException(); + listener.onFailure(headerParser.getRequest(), failure); + return true; + } + private void reset() { headerParser.reset(); - state = State.HEADER; + state = State.INITIAL; padding = 0; } @@ -190,6 +206,6 @@ public abstract class Parser private enum State { - HEADER, CONTENT, PADDING + INITIAL, HEADER, CONTENT, PADDING } } diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/ServerFCGIConnection.java b/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/ServerFCGIConnection.java index ef2cac46f60..25b85a60fca 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/ServerFCGIConnection.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/internal/ServerFCGIConnection.java @@ -36,7 +36,6 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.util.Attributes; -import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -156,12 +155,6 @@ public class ServerFCGIConnection extends AbstractConnection implements Connecti return getEndPoint().getLocalSocketAddress(); } - @Override - public HostPort getServerAuthority() - { - return ConnectionMetaData.getServerAuthority(configuration, this); - } - @Override public Object removeAttribute(String name) { @@ -270,6 +263,8 @@ public class ServerFCGIConnection extends AbstractConnection implements Connecti private void releaseInputBuffer() { + if (networkBuffer == null) + return; boolean released = networkBuffer.release(); if (LOG.isDebugEnabled()) LOG.debug("releaseInputBuffer {} {}", released, this); @@ -327,6 +322,9 @@ public class ServerFCGIConnection extends AbstractConnection implements Connecti @Override public boolean onIdleExpired(TimeoutException timeoutException) { + HttpStreamOverFCGI stream = this.stream; + if (stream == null) + return true; Runnable task = stream.getHttpChannel().onIdleTimeout(timeoutException); if (task != null) getExecutor().execute(task); diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java b/jetty-core/jetty-fcgi/jetty-fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java index 83c3a75cb55..fba92ebd364 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java @@ -32,7 +32,9 @@ import java.util.zip.GZIPOutputStream; import org.eclipse.jetty.client.AsyncRequestContent; import org.eclipse.jetty.client.BytesRequestContent; +import org.eclipse.jetty.client.ConnectionPool; import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.Destination; import org.eclipse.jetty.client.FutureResponseListener; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.Request; @@ -515,6 +517,35 @@ public class HttpClientTest extends AbstractHttpClientServerTest @Test public void testConnectionIdleTimeout() throws Exception + { + long idleTimeout = 1000; + start(new Handler.Abstract() + { + @Override + public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) + { + callback.succeeded(); + return true; + } + }); + connector.setIdleTimeout(idleTimeout); + + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .timeout(2 * idleTimeout, TimeUnit.MILLISECONDS) + .send(); + assertNotNull(response); + assertEquals(200, response.getStatus()); + + Thread.sleep(2 * idleTimeout); + + assertTrue(client.getDestinations().stream() + .map(Destination::getConnectionPool) + .allMatch(ConnectionPool::isEmpty)); + } + + @Test + public void testConnectionIdleTimeoutIgnored() throws Exception { long idleTimeout = 1000; start(new Handler.Abstract() @@ -522,9 +553,11 @@ public class HttpClientTest extends AbstractHttpClientServerTest @Override public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) throws Exception { - // Handler says it will handle the idletimeout + // Handler says it will handle the idle timeout by ignoring it. request.addIdleTimeoutListener(t -> false); - TimeUnit.MILLISECONDS.sleep(2 * idleTimeout); + // Sleep an non-integral number of idle timeouts to avoid + // racing with the idle timeout ticking every idle period. + TimeUnit.MILLISECONDS.sleep(idleTimeout * 3 / 2); callback.succeeded(); return true; } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index f173ff2e87f..2b88b73a919 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -774,7 +774,7 @@ public class HttpGenerator } if (LOG.isDebugEnabled()) - LOG.debug(_endOfContent.toString()); + LOG.debug("endOfContent {} content-Length {}", _endOfContent.toString(), contentLength); // Add transfer encoding if it is not chunking if (transferEncoding != null) 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 14ec49ea3fd..064f343e063 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 @@ -656,7 +656,7 @@ public class MultiPart @Override public long getLength() { - // TODO: it is difficult to calculate the length because + // TODO: #10307 it is difficult to calculate the length because // we need to allow for customization of the headers from // subclasses, and then serialize all the headers to get // their length (handling UTF-8 values) and we don't want diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java index 289bc28a503..3ea92b23caa 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java @@ -51,7 +51,6 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; @@ -417,12 +416,6 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection return getEndPoint().getLocalSocketAddress(); } - @Override - public HostPort getServerAuthority() - { - return ConnectionMetaData.getServerAuthority(httpConfig, this); - } - @Override public Object getAttribute(String name) { diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java index ff914e0590d..2d906197bf9 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java @@ -14,11 +14,17 @@ package org.eclipse.jetty.io; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.ByteBuffer; +import java.time.Instant; import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.IntUnaryOperator; +import java.util.stream.Collectors; import org.eclipse.jetty.io.internal.CompoundPool; import org.eclipse.jetty.io.internal.QueuedPool; @@ -564,4 +570,112 @@ public class ArrayByteBufferPool implements ByteBufferPool, Dumpable ); } } + + /** + *

A variant of {@link ArrayByteBufferPool} that tracks buffer + * acquires/releases, useful to identify buffer leaks.

+ *

Use {@link #getLeaks()} when the system is idle to get + * the {@link Buffer}s that have been leaked, which contain + * the stack trace information of where the buffer was acquired.

+ */ + public static class Tracking extends ArrayByteBufferPool + { + private static final Logger LOG = LoggerFactory.getLogger(Tracking.class); + + private final Set buffers = ConcurrentHashMap.newKeySet(); + + public Tracking() + { + this(0, -1, Integer.MAX_VALUE); + } + + public Tracking(int minCapacity, int maxCapacity, int maxBucketSize) + { + this(minCapacity, maxCapacity, maxBucketSize, -1L, -1L); + } + + public Tracking(int minCapacity, int maxCapacity, int maxBucketSize, long maxHeapMemory, long maxDirectMemory) + { + super(minCapacity, -1, maxCapacity, maxBucketSize, maxHeapMemory, maxDirectMemory); + } + + @Override + public RetainableByteBuffer acquire(int size, boolean direct) + { + RetainableByteBuffer buffer = super.acquire(size, direct); + Buffer wrapper = new Buffer(buffer, size); + if (LOG.isDebugEnabled()) + LOG.debug("acquired {}", wrapper); + buffers.add(wrapper); + return wrapper; + } + + public Set getLeaks() + { + return buffers; + } + + public String dumpLeaks() + { + return getLeaks().stream() + .map(Buffer::dump) + .collect(Collectors.joining(System.lineSeparator())); + } + + public class Buffer extends RetainableByteBuffer.Wrapper + { + private final int size; + private final Instant acquireInstant; + private final Throwable acquireStack; + + private Buffer(RetainableByteBuffer wrapped, int size) + { + super(wrapped); + this.size = size; + this.acquireInstant = Instant.now(); + this.acquireStack = new Throwable(); + } + + public int getSize() + { + return size; + } + + public Instant getAcquireInstant() + { + return acquireInstant; + } + + public Throwable getAcquireStack() + { + return acquireStack; + } + + @Override + public boolean release() + { + boolean released = super.release(); + if (released) + { + buffers.remove(this); + if (LOG.isDebugEnabled()) + LOG.debug("released {}", this); + } + return released; + } + + public String dump() + { + StringWriter w = new StringWriter(); + getAcquireStack().printStackTrace(new PrintWriter(w)); + return "%s of %d bytes on %s at %s".formatted(getClass().getSimpleName(), getSize(), getAcquireInstant(), w); + } + + @Override + public String toString() + { + return "%s@%x[%s]".formatted(getClass().getSimpleName(), hashCode(), super.toString()); + } + } + } } 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 1f7fd012420..7b1868d0fc5 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 @@ -42,6 +42,7 @@ import org.eclipse.jetty.io.content.PathContentSource; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.CompletableTask; import org.eclipse.jetty.util.FutureCallback; import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.IO; @@ -104,7 +105,9 @@ public class ContentSourceTest return List.of(asyncSource, byteBufferSource, transformerSource, pathSource, inputSource, inputSource2); } - /** Get the next chunk, blocking if necessary + /** + * Get the next chunk, blocking if necessary + * * @param source The source to get the next chunk from * @return A non null chunk */ @@ -113,8 +116,7 @@ public class ContentSourceTest Content.Chunk chunk = source.read(); if (chunk != null) return chunk; - FuturePromise next = new FuturePromise<>(); - Runnable getNext = new Runnable() + CompletableTask task = new CompletableTask<>() { @Override public void run() @@ -122,18 +124,12 @@ public class ContentSourceTest Content.Chunk chunk = source.read(); if (chunk == null) source.demand(this); - next.succeeded(chunk); + else + complete(chunk); } }; - source.demand(getNext); - try - { - return next.get(); - } - catch (Exception e) - { - throw new RuntimeException(e); - } + source.demand(task); + return task.join(); } @ParameterizedTest @@ -141,8 +137,7 @@ public class ContentSourceTest public void testRead(Content.Source source) throws Exception { StringBuilder builder = new StringBuilder(); - CountDownLatch eof = new CountDownLatch(1); - source.demand(new Runnable() + var task = new CompletableTask<>() { @Override public void run() @@ -162,14 +157,14 @@ public class ContentSourceTest if (chunk.isLast()) { - eof.countDown(); + complete(null); break; } } } - }); - - assertTrue(eof.await(10, TimeUnit.SECONDS)); + }; + source.demand(task); + task.get(10, TimeUnit.SECONDS); assertThat(builder.toString(), is("onetwo")); } diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java index 2e37313241b..3309f2f6da4 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/internal/DeferredAuthenticationState.java @@ -181,6 +181,12 @@ public class DeferredAuthenticationState implements AuthenticationState.Deferred return true; } + @Override + public boolean hasLastWrite() + { + return false; + } + @Override public boolean isCompletedSuccessfully() { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java index 7c7b5132050..c48a010082d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java @@ -67,20 +67,21 @@ public interface ConnectionMetaData extends Attributes /** * @return The URI authority that this server represents. By default, this is the address of the network socket on - * which the connection was accepted, but it may be wrapped to represent a virtual address. + * which the connection was accepted, but it may be configured to a specific address. + * @see HttpConfiguration#setServerAuthority(HostPort) */ - HostPort getServerAuthority(); - - static HostPort getServerAuthority(HttpConfiguration httpConfiguration, ConnectionMetaData connectionMetaData) + default HostPort getServerAuthority() { + HttpConfiguration httpConfiguration = getHttpConfiguration(); HostPort authority = httpConfiguration.getServerAuthority(); if (authority != null) return authority; - SocketAddress local = connectionMetaData.getLocalSocketAddress(); - if (local instanceof InetSocketAddress inet) - return new HostPort(inet.getHostString(), inet.getPort()); - + SocketAddress localSocketAddress = getLocalSocketAddress(); + if (localSocketAddress instanceof InetSocketAddress inetSocketAddress) + return new HostPort(inetSocketAddress.getHostString(), inetSocketAddress.getPort()); + else if (localSocketAddress != null) + return new HostPort(localSocketAddress.toString()); return null; } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java index d2cc7560728..548db964aa3 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java @@ -42,7 +42,7 @@ import org.slf4j.LoggerFactory; *

This factory can be placed in front of any other connection factory * to process the proxy v1 or v2 line before the normal protocol handling

* - * @see http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt + * @see PROXY protocol */ public class ProxyConnectionFactory extends DetectorConnectionFactory { @@ -245,6 +245,7 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory _buffer.release(); return unconsumed; } + _buffer.release(); return null; } @@ -564,6 +565,7 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory _buffer.release(); return unconsumed; } + _buffer.release(); return null; } @@ -591,7 +593,7 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory SocketAddress remote; switch (_family) { - case INET: + case INET -> { byte[] addr = new byte[4]; byteBuffer.get(addr); @@ -602,9 +604,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory int dstPort = byteBuffer.getChar(); local = new InetSocketAddress(dstAddr, dstPort); remote = new InetSocketAddress(srcAddr, srcPort); - break; } - case INET6: + case INET6 -> { byte[] addr = new byte[16]; byteBuffer.get(addr); @@ -615,9 +616,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory int dstPort = byteBuffer.getChar(); local = new InetSocketAddress(dstAddr, dstPort); remote = new InetSocketAddress(srcAddr, srcPort); - break; } - case UNIX: + case UNIX -> { byte[] addr = new byte[108]; byteBuffer.get(addr); @@ -626,12 +626,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory String dst = UnixDomain.toPath(addr); local = UnixDomain.newSocketAddress(dst); remote = UnixDomain.newSocketAddress(src); - break; - } - default: - { - throw new IllegalStateException("Unsupported family " + _family); } + default -> throw new IllegalStateException("Unsupported family " + _family); } proxyEndPoint = new ProxyEndPoint(endPoint, local, remote); @@ -714,37 +710,20 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory int transportAndFamily = 0xFF & byteBuffer.get(); switch (transportAndFamily >> 4) { - case 0: - _family = Family.UNSPEC; - break; - case 1: - _family = Family.INET; - break; - case 2: - _family = Family.INET6; - break; - case 3: - _family = Family.UNIX; - break; - default: - throw new IOException("Proxy v2 bad PROXY family"); + case 0 -> _family = Family.UNSPEC; + case 1 -> _family = Family.INET; + case 2 -> _family = Family.INET6; + case 3 -> _family = Family.UNIX; + default -> throw new IOException("Proxy v2 bad PROXY family"); } - Transport transport; - switch (transportAndFamily & 0xF) + Transport transport = switch (transportAndFamily & 0xF) { - case 0: - transport = Transport.UNSPEC; - break; - case 1: - transport = Transport.STREAM; - break; - case 2: - transport = Transport.DGRAM; - break; - default: - throw new IOException("Proxy v2 bad PROXY family"); - } + case 0 -> Transport.UNSPEC; + case 1 -> Transport.STREAM; + case 2 -> Transport.DGRAM; + default -> throw new IOException("Proxy v2 bad PROXY family"); + }; _length = byteBuffer.getChar(); @@ -761,6 +740,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory private void releaseAndClose() { + if (LOG.isDebugEnabled()) + LOG.debug("Proxy v2 releasing buffer and closing"); _buffer.release(); close(); } 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 e0ae22a8f4e..c533bc989fb 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 @@ -403,6 +403,12 @@ public interface Request extends Attributes, Content.Source return -1; } + /** + * Get the logical name the request was sent to, which may be from the authority of the + * request; the configured server authority; the actual network name of the server; + * @param request The request to get the server name of + * @return The logical server name or null if it cannot be determined. + */ static String getServerName(Request request) { if (request == null) @@ -414,38 +420,42 @@ public interface Request extends Attributes, Content.Source HostPort authority = request.getConnectionMetaData().getServerAuthority(); if (authority != null) - return HostPort.normalizeHost(authority.getHost()); + return authority.getHost(); - SocketAddress local = request.getConnectionMetaData().getLocalSocketAddress(); - if (local instanceof InetSocketAddress) - return HostPort.normalizeHost(((InetSocketAddress)local).getHostString()); - - return local == null ? null : local.toString(); + return null; } + /** + * Get the logical port a request was received on, which may be from the authority of the request; the + * configured server authority; the default port for the scheme; or the actual network port. + * @param request The request to get the port of + * @return The port for the request if it can be determined, otherwise -1 + */ static int getServerPort(Request request) { if (request == null) return -1; + + // Does the request have an explicit port? HttpURI uri = request.getHttpURI(); if (uri.hasAuthority() && uri.getPort() > 0) return uri.getPort(); - HostPort authority = request.getConnectionMetaData().getServerAuthority(); + // Is there a configured server authority? + HostPort authority = request.getConnectionMetaData().getHttpConfiguration().getServerAuthority(); if (authority != null && authority.getPort() > 0) return authority.getPort(); - if (authority == null) - { - SocketAddress local = request.getConnectionMetaData().getLocalSocketAddress(); - if (local instanceof InetSocketAddress) - return ((InetSocketAddress)local).getPort(); - } - + // Is there a scheme with a default port? HttpScheme scheme = HttpScheme.CACHE.get(request.getHttpURI().getScheme()); - if (scheme != null) + if (scheme != null && scheme.getDefaultPort() > 0) return scheme.getDefaultPort(); + // Is there a local port? + SocketAddress local = request.getConnectionMetaData().getLocalSocketAddress(); + if (local instanceof InetSocketAddress inetSocketAddress && inetSocketAddress.getPort() > 0) + return inetSocketAddress.getPort(); + return -1; } @@ -770,11 +780,11 @@ public interface Request extends Attributes, Content.Source @SuppressWarnings("unchecked") static T as(Request request, Class type) { - while (request instanceof Request.Wrapper wrapper) + while (request != null) { - if (type.isInstance(wrapper)) - return (T)wrapper; - request = wrapper.getWrapped(); + if (type.isInstance(request)) + return (T)request; + request = request instanceof Request.Wrapper wrapper ? wrapper.getWrapped() : null; } return null; } 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 b737459b939..1024b2e5455 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 @@ -145,7 +145,7 @@ public class ResourceService return _gzipEquivalentFileExtensions; } - public void doGet(Request request, Response response, Callback callback, HttpContent content) throws Exception + public void doGet(Request request, Response response, Callback callback, HttpContent content) { String pathInContext = Request.getPathInContext(request); @@ -523,7 +523,7 @@ public class ResourceService // TODO : check conditional headers. serveWelcome(request, response, callback, welcomeAction.target); case REHANDLE -> rehandleWelcome(request, response, callback, welcomeAction.target); - }; + } } /** @@ -683,14 +683,15 @@ public class ResourceService } // There are multiple non-overlapping ranges, send a multipart/byteranges 206 response. - putHeaders(response, content, NO_CONTENT_LENGTH); response.setStatus(HttpStatus.PARTIAL_CONTENT_206); String contentType = "multipart/byteranges; boundary="; String boundary = MultiPart.generateBoundary(null, 24); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType + boundary); MultiPartByteRanges.ContentSource byteRanges = new MultiPartByteRanges.ContentSource(boundary); ranges.forEach(range -> byteRanges.addPart(new MultiPartByteRanges.Part(content.getContentTypeValue(), content.getResource().getPath(), range, contentLength))); byteRanges.close(); + long partsContentLength = byteRanges.getLength(); + putHeaders(response, content, partsContentLength); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType + boundary); Content.copy(byteRanges, response, callback); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index ab71311d89a..c1eedf615a1 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -99,6 +99,13 @@ public interface Response extends Content.Sink */ boolean isCommitted(); + /** + *

Returns whether the last write has been initiated on the response.

+ * + * @return {@code true} if {@code last==true} has been passed to {@link #write(boolean, ByteBuffer, Callback)}. + */ + boolean hasLastWrite(); + /** *

Returns whether the response completed successfully.

*

The response HTTP status code, HTTP headers and content @@ -207,13 +214,13 @@ public interface Response extends Content.Sink * @see Wrapper */ @SuppressWarnings("unchecked") - static T as(Response response, Class type) + static T as(Response response, Class type) { - while (response instanceof Response.Wrapper wrapper) + while (response != null) { - if (type.isInstance(wrapper)) - return (T)wrapper; - response = wrapper.getWrapped(); + if (type.isInstance(response)) + return (T)response; + response = response instanceof Response.Wrapper wrapper ? wrapper.getWrapped() : null; } return null; } @@ -580,6 +587,12 @@ public interface Response extends Content.Sink return getWrapped().isCommitted(); } + @Override + public boolean hasLastWrite() + { + return getWrapped().hasLastWrite(); + } + @Override public boolean isCompletedSuccessfully() { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ReHandlingErrorHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ReHandlingErrorHandler.java index 9c87501b056..46868279f90 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ReHandlingErrorHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ReHandlingErrorHandler.java @@ -69,8 +69,7 @@ public abstract class ReHandlingErrorHandler extends ErrorHandler { if (LOG.isDebugEnabled()) LOG.debug("Unable to process error {}", reRequest, e); - if (ExceptionUtil.areNotAssociated(cause, e)) - cause.addSuppressed(e); + ExceptionUtil.addSuppressedIfNotAssociated(cause, e); response.setStatus(code); } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipResponseAndCallback.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipResponseAndCallback.java index bd311d157cc..f8ff65f26ab 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipResponseAndCallback.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipResponseAndCallback.java @@ -136,7 +136,13 @@ public class GzipResponseAndCallback extends Response.Wrapper implements Callbac case NOT_COMPRESSING -> super.write(last, content, callback); case COMMITTING -> callback.failed(new WritePendingException()); case COMPRESSING -> gzip(last, callback, content); - default -> callback.failed(new IllegalStateException("state=" + _state.get())); + default -> + { + if (BufferUtil.isEmpty(content)) + callback.succeeded(); + else + callback.failed(new IllegalStateException("state=" + _state.get())); + } } } 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 8e119a838f4..3fc69037626 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 @@ -473,8 +473,7 @@ public class HttpChannelState implements HttpChannel, Components } catch (Throwable throwable) { - if (ExceptionUtil.areNotAssociated(x, throwable)) - x.addSuppressed(throwable); + ExceptionUtil.addSuppressedIfNotAssociated(x, throwable); } // If the application has not been otherwise informed of the failure @@ -1080,8 +1079,7 @@ public class HttpChannelState implements HttpChannel, Components } catch (Throwable t) { - if (ExceptionUtil.areNotAssociated(throwable, t)) - throwable.addSuppressed(t); + ExceptionUtil.addSuppressedIfNotAssociated(throwable, t); } finally { @@ -1354,6 +1352,18 @@ public class HttpChannelState implements HttpChannel, Components return _httpFields.isCommitted(); } + @Override + public boolean hasLastWrite() + { + try (AutoLock ignored = _request._lock.lock()) + { + if (_request._httpChannelState == null) + return true; + + return _request._httpChannelState._streamSendState != StreamSendState.SENDING; + } + } + @Override public boolean isCompletedSuccessfully() { @@ -1540,8 +1550,7 @@ public class HttpChannelState implements HttpChannel, Components // Consume any input. Throwable unconsumed = stream.consumeAvailable(); - if (ExceptionUtil.areNotAssociated(unconsumed, failure)) - failure.addSuppressed(unconsumed); + ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed); if (LOG.isDebugEnabled()) LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", httpChannelState._stream.isCommitted(), httpChannelState._response.isCommitted(), this); @@ -1689,8 +1698,7 @@ public class HttpChannelState implements HttpChannel, Components Callback.from(() -> httpChannelState._handlerInvoker.failed(failure), x -> { - if (ExceptionUtil.areNotAssociated(failure, x)) - failure.addSuppressed(x); + ExceptionUtil.addSuppressedIfNotAssociated(failure, x); httpChannelState._handlerInvoker.failed(failure); })); } @@ -1758,8 +1766,7 @@ public class HttpChannelState implements HttpChannel, Components } catch (Throwable t) { - if (ExceptionUtil.areNotAssociated(failure, t)) - failure.addSuppressed(t); + ExceptionUtil.addSuppressedIfNotAssociated(failure, t); super.onError(task, 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 3034d4d1f9f..8c738937568 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 @@ -288,15 +288,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ return getEndPoint().getLocalSocketAddress(); } - @Override - public HostPort getServerAuthority() - { - HostPort authority = ConnectionMetaData.getServerAuthority(getHttpConfiguration(), this); - if (authority == null) - authority = new HostPort(getLocalSocketAddress().toString(), -1); - return authority; - } - @Override public Object removeAttribute(String name) { diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java index bd721995668..7f1e65270f7 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java @@ -14,6 +14,8 @@ package org.eclipse.jetty.server; import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -22,6 +24,9 @@ import java.util.stream.Stream; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.internal.HttpConnection; import org.eclipse.jetty.util.Callback; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -75,7 +80,25 @@ public class ForwardedRequestCustomizerTest server.addConnector(connector); // Alternate behavior Connector - HttpConnectionFactory httpAlt = new HttpConnectionFactory(); + HttpConnectionFactory httpAlt = new HttpConnectionFactory() + { + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) + { + HttpConnection connection = new HttpConnection(getHttpConfiguration(), connector, endPoint, isRecordHttpComplianceViolations()) + { + @Override + public SocketAddress getLocalSocketAddress() + { + return InetSocketAddress.createUnresolved("test", 42); + } + }; + connection.setUseInputDirectByteBuffers(isUseInputDirectByteBuffers()); + connection.setUseOutputDirectByteBuffers(isUseOutputDirectByteBuffers()); + return configure(connection, connector, endPoint); + } + }; + httpAlt.getHttpConfiguration().setSecurePort(8443); httpAlt.getHttpConfiguration().setHttpCompliance(mismatchedAuthorityHttpCompliance); customizerAlt = new ForwardedRequestCustomizer(); @@ -128,27 +151,6 @@ public class ForwardedRequestCustomizerTest server.stop(); } - public static Stream cases2() - { - return Stream.of( - Arguments.of(new TestRequest("https initial authority, X-Forwarded-Proto on http, Proxy-Ssl-Id exists (setSslIsSecure==false)") - .configureCustomizer((customizer) -> customizer.setSslIsSecure(false)) - .headers( - "GET https://alt.example.net/foo HTTP/1.1", - "Host: alt.example.net", - "X-Forwarded-Proto: http", - "Proxy-Ssl-Id: Wibble" - ), - new Expectations() - .scheme("http").serverName("alt.example.net").serverPort(80) - .secure(false) - .requestURL("http://alt.example.net/foo") - .remoteAddr("0.0.0.0").remotePort(0) - .sslSession("Wibble") - ) - ); - } - public static Stream cases() { return Stream.of( @@ -1032,7 +1034,6 @@ public class ForwardedRequestCustomizerTest request.configure(customizer); String rawRequest = request.getRawRequest((header) -> header); -// System.out.println(rawRequest); HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest)); assertThat("status", response.getStatus(), is(200)); @@ -1052,7 +1053,6 @@ public class ForwardedRequestCustomizerTest .replaceFirst("X-Proxied-Https:", "Jetty-Proxied-Https:") .replaceFirst("Proxy-Ssl-Id:", "Jetty-Proxy-Ssl-Id:") .replaceFirst("Proxy-auth-cert:", "Jetty-Proxy-Auth-Cert:")); - // System.out.println(rawRequest); HttpTester.Response response = HttpTester.parseResponse(connectorConfigured.getResponse(rawRequest)); assertThat("status", response.getStatus(), is(200)); @@ -1063,6 +1063,40 @@ public class ForwardedRequestCustomizerTest public static Stream nonStandardPortCases() { return Stream.of( + // HTTP 1.0 + Arguments.of( + new TestRequest("HTTP/1.0 - no Host header") + .headers( + "GET /example HTTP/1.0" + ), + new Expectations() + .scheme("http").serverName("test").serverPort(42) + .secure(false) + .requestURL("http://test:42/example") + ), + Arguments.of( + new TestRequest("HTTP/1.0 - Empty Host header") + .headers( + "GET scheme:///example HTTP/1.0", + "Host:" + ), + new Expectations() + .scheme("scheme").serverName(null).serverPort(42) + .secure(false) + .requestURL("scheme:///example") + ), + Arguments.of( + new TestRequest("HTTP/1.0 - Host header") + .headers( + "GET /example HTTP/1.0", + "Host: server:9999" + ), + new Expectations() + .scheme("http").serverName("server").serverPort(9999) + .secure(false) + .requestURL("http://server:9999/example") + ), + // RFC7239 Tests with https. Arguments.of(new TestRequest("RFC7239 with https and h2") .headers( @@ -1106,7 +1140,7 @@ public class ForwardedRequestCustomizerTest /** * Tests against a Connector with a HttpConfiguration on non-standard ports. - * HttpConfiguration is set to securePort of 8443 + * HttpConfiguration is set to securePort of 8443 and the local port is 42. */ @ParameterizedTest(name = "{0}") @MethodSource("nonStandardPortCases") 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 745de4c3ada..8c138113014 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 @@ -19,7 +19,6 @@ import java.nio.channels.SocketChannel; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.http.ByteRange; import org.eclipse.jetty.http.HttpField; @@ -31,7 +30,6 @@ import org.eclipse.jetty.http.MultiPart; import org.eclipse.jetty.http.MultiPartByteRanges; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.io.content.ByteBufferContentSource; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; @@ -49,13 +47,13 @@ public class MultiPartByteRangesTest { private Server server; private ServerConnector connector; - private LeakTrackingBufferPool byteBufferPool; + private ArrayByteBufferPool.Tracking byteBufferPool; private void start(Handler handler) throws Exception { QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); - byteBufferPool = new LeakTrackingBufferPool(); + byteBufferPool = new ArrayByteBufferPool.Tracking(); server = new Server(serverThreads, null, byteBufferPool); connector = new ServerConnector(server, 1, 1); server.addConnector(connector); @@ -67,7 +65,7 @@ public class MultiPartByteRangesTest public void dispose() { LifeCycle.stop(server); - assertEquals(0, byteBufferPool.countLeaks()); + assertEquals(0, byteBufferPool.getLeaks().size()); } @Test @@ -131,31 +129,4 @@ public class MultiPartByteRangesTest assertEquals("CDEF", Content.Source.asString(part3.getContentSource())); } } - - private static class LeakTrackingBufferPool extends ArrayByteBufferPool - { - private final AtomicInteger leaks = new AtomicInteger(); - - public int countLeaks() - { - return leaks.get(); - } - - @Override - public RetainableByteBuffer acquire(int size, boolean direct) - { - leaks.incrementAndGet(); - return new RetainableByteBuffer.Wrapper(super.acquire(size, direct)) - { - @Override - public boolean release() - { - boolean released = super.release(); - if (released) - leaks.decrementAndGet(); - return released; - } - }; - } - } } diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java index b0a460e81e4..2ab2ec06f6e 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java @@ -207,7 +207,7 @@ public class BaseBuilder List startLines = new ArrayList<>(); for (Path path : paths) { - StartLog.info("copy " + baseHome.toShortForm(path) + " into " + baseHome.toShortForm(startini)); + StartLog.info("copy %s into %s", baseHome.toShortForm(path), baseHome.toShortForm(startini)); startLines.add(""); startLines.add("# Config from " + baseHome.toShortForm(path)); startLines.addAll(Files.readAllLines(path)); @@ -250,7 +250,7 @@ public class BaseBuilder if (FS.ensureDirectoryExists(startd)) { - StartLog.info("mkdir " + baseHome.toShortForm(startd)); + StartLog.info("mkdir %s", baseHome.toShortForm(startd)); modified.set(true); } diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FS.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FS.java index 559161a8ebe..a1608d48f8c 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FS.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FS.java @@ -23,12 +23,9 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; -import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jetty.util.FileID; @@ -130,7 +127,7 @@ public class FS if (!Files.isDirectory(path)) { // not a directory (as expected) - StartLog.warn("Not a directory: " + path); + StartLog.warn("Not a directory: %s", path); return false; } diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FileInitializer.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FileInitializer.java index 0d26cdcf894..7dd9a2b788a 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FileInitializer.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/FileInitializer.java @@ -14,14 +14,10 @@ package org.eclipse.jetty.start; import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URI; -import java.net.URLConnection; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.HashSet; import java.util.Set; @@ -167,7 +163,7 @@ public abstract class FileInitializer { if (FS.ensureDirectoryExists(to)) { - StartLog.info("mkdir " + _basehome.toShortForm(to)); + StartLog.info("mkdir %s", _basehome.toShortForm(to)); modified = true; } diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index 4ccc2325ac3..c7bf741455a 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -186,8 +186,8 @@ public class Main StartLog.error("Do not start with ${jetty.base} == ${jetty.home}!"); else StartLog.error("No enabled jetty modules found!"); - StartLog.info("${jetty.home} = " + getBaseHome().getHomePath()); - StartLog.info("${jetty.base} = " + getBaseHome().getBasePath()); + StartLog.info("${jetty.home} = %s", getBaseHome().getHomePath()); + StartLog.info("${jetty.base} = %s", getBaseHome().getBasePath()); StartLog.error("Please create and/or configure a ${jetty.base} directory."); usageExit(ERR_INVOKE_MAIN); return; @@ -201,7 +201,7 @@ public class Main } catch (ClassNotFoundException e) { - StartLog.error("Unable to find: " + mainclass); + StartLog.error("Unable to find: %s", mainclass); StartLog.debug(e); usageExit(ERR_INVOKE_MAIN); return; @@ -489,7 +489,7 @@ public class Main final Process process = pbuilder.start(); Runtime.getRuntime().addShutdownHook(new Thread(() -> { - StartLog.debug("Destroying " + process); + StartLog.debug("Destroying %s", process); process.destroy(); })); @@ -685,7 +685,7 @@ public class Main } else { - StartLog.warn("Unable to find resource: " + resourceName); + StartLog.warn("Unable to find resource: %s", resourceName); } } catch (IOException e) diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathFinder.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathFinder.java index c9fb2c16f15..a139e142914 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathFinder.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathFinder.java @@ -42,7 +42,7 @@ public class PathFinder extends SimpleFileVisitor private void addHit(Path path) { String relPath = basePath.relativize(path).toString(); - StartLog.debug("Found [" + relPath + "] " + path); + StartLog.debug("Found [%s] %s", relPath, path); hits.put(relPath, path); } @@ -76,7 +76,7 @@ public class PathFinder extends SimpleFileVisitor { if (dirMatcher.matches(dir)) { - StartLog.trace("Following dir: " + dir); + StartLog.trace("Following dir: %s", dir); if (includeDirsInResults && fileMatcher.matches(dir)) { addHit(dir); @@ -85,7 +85,7 @@ public class PathFinder extends SimpleFileVisitor } else { - StartLog.trace("Skipping dir: " + dir); + StartLog.trace("Skipping dir: %s", dir); return FileVisitResult.SKIP_SUBTREE; } } @@ -131,7 +131,7 @@ public class PathFinder extends SimpleFileVisitor } else { - StartLog.trace("Ignoring file: " + file); + StartLog.trace("Ignoring file: %s", file); } return FileVisitResult.CONTINUE; } @@ -143,7 +143,7 @@ public class PathFinder extends SimpleFileVisitor { if (!NOTIFIED_PATHS.contains(file)) { - StartLog.warn("skipping detected filesystem loop: " + file); + StartLog.warn("skipping detected filesystem loop: %s", file); NOTIFIED_PATHS.add(file); } return FileVisitResult.SKIP_SUBTREE; diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathMatchers.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathMatchers.java index 27766f6d19c..437fbcd06cb 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathMatchers.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/PathMatchers.java @@ -84,7 +84,7 @@ public class PathMatchers // use FileSystem default pattern behavior if (pattern.startsWith("glob:") || pattern.startsWith("regex:")) { - StartLog.debug("Using Standard " + fs.getClass().getName() + " pattern: " + pattern); + StartLog.debug("Using Standard %s pattern: %s", fs.getClass().getName(), pattern); return fs.getPathMatcher(pattern); } @@ -93,14 +93,14 @@ public class PathMatchers if (isAbsolute(pattern)) { String pat = "glob:" + pattern; - StartLog.debug("Using absolute path pattern: " + pat); + StartLog.debug("Using absolute path pattern: %s", pat); return fs.getPathMatcher(pat); } // Doesn't start with filesystem root, then assume the pattern // is a relative file path pattern. String pat = "glob:**/" + pattern; - StartLog.debug("Using relative path pattern: " + pat); + StartLog.debug("Using relative path pattern: %s", pat); return fs.getPathMatcher(pat); } diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java index 8e6e255c171..30d73fa2f08 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java @@ -428,9 +428,9 @@ public class StartArgs for (String rawlibref : module.getLibs()) { - StartLog.debug("rawlibref = " + rawlibref); + StartLog.debug("rawlibref = %s", rawlibref); String libref = environment.getProperties().expand(rawlibref); - StartLog.debug("expanded = " + libref); + StartLog.debug("expanded = %s", libref); for (Path libpath : baseHome.getPaths(libref)) { @@ -555,6 +555,7 @@ public class StartArgs if (parts.contains("path")) { Classpath classpath = jettyEnvironment.getClasspath(); + StartLog.debug("classpath=%s - isJPMS=%b", classpath, isJPMS()); if (isJPMS()) { Map> dirsAndFiles = StreamSupport.stream(classpath.spliterator(), false) @@ -605,6 +606,7 @@ public class StartArgs } generateJpmsArgs(cmd); + StartLog.debug("JPMS resulting cmd=%s", cmd.toCommandLine()); } else if (!classpath.isEmpty()) { @@ -1255,7 +1257,7 @@ public class StartArgs if (arg.startsWith("--add-to-start=") || arg.startsWith("--add-to-startd=")) { String value = Props.getValue(arg); - StartLog.warn("Option " + arg.split("=")[0] + " is deprecated! Instead use: --add-modules=%s", value); + StartLog.warn("Option %s is deprecated! Instead use: --add-modules=%s", arg.split("=")[0], value); } startModules.addAll(Props.getValues(arg)); run = false; diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartEnvironment.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartEnvironment.java index 6c328cdf595..a0ee205ca13 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartEnvironment.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartEnvironment.java @@ -231,9 +231,9 @@ public class StartEnvironment StartLog.debug("Expanding Libs"); for (String rawlibref : _libRefs) { - StartLog.debug("rawlibref = " + rawlibref); + StartLog.debug("rawlibref = %s", rawlibref); String libref = getProperties().expand(rawlibref); - StartLog.debug("expanded = " + libref); + StartLog.debug("expanded = %s", libref); // perform path escaping (needed by windows) libref = libref.replaceAll("\\\\([^\\\\])", "\\\\\\\\$1"); diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/builders/StartDirBuilder.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/builders/StartDirBuilder.java index 528c77921d3..909ab543e2d 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/builders/StartDirBuilder.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/builders/StartDirBuilder.java @@ -42,7 +42,7 @@ public class StartDirBuilder implements BaseBuilder.Config this.baseHome = baseBuilder.getBaseHome(); this.startDir = baseHome.getBasePath("start.d"); if (FS.ensureDirectoryExists(startDir)) - StartLog.info("mkdir " + baseHome.toShortForm(startDir)); + StartLog.info("mkdir %s", baseHome.toShortForm(startDir)); } @Override diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/BaseHomeFileInitializer.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/BaseHomeFileInitializer.java index 202e228e921..55a5eb59e54 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/BaseHomeFileInitializer.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/BaseHomeFileInitializer.java @@ -64,7 +64,7 @@ public class BaseHomeFileInitializer extends FileInitializer else if (FS.ensureDirectoryExists(destination)) { modified = true; - StartLog.info("mkdir " + _basehome.toShortForm(destination)); + StartLog.info("mkdir %s", _basehome.toShortForm(destination)); } copyDirectory(source, destination); @@ -74,7 +74,7 @@ public class BaseHomeFileInitializer extends FileInitializer if (FS.ensureDirectoryExists(destination.getParent())) { modified = true; - StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent())); + StartLog.info("mkdir %s", _basehome.toShortForm(destination.getParent())); } if (!FS.exists(destination)) diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/DownloadFileInitializer.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/DownloadFileInitializer.java index c6aca076d95..738c7999b77 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/DownloadFileInitializer.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/DownloadFileInitializer.java @@ -61,7 +61,7 @@ public abstract class DownloadFileInitializer extends FileInitializer } if (FS.ensureDirectoryExists(destination.getParent())) - StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent())); + StartLog.info("mkdir %s", _basehome.toShortForm(destination.getParent())); StartLog.info("download %s to %s", uri, _basehome.toShortForm(destination)); diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/LocalFileInitializer.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/LocalFileInitializer.java index de084099132..9769d8cf8bc 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/LocalFileInitializer.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/LocalFileInitializer.java @@ -73,7 +73,7 @@ public class LocalFileInitializer extends FileInitializer // Create directory boolean mkdir = FS.ensureDirectoryExists(destination); if (mkdir) - StartLog.info("mkdir " + _basehome.toShortForm(destination)); + StartLog.info("mkdir %s", _basehome.toShortForm(destination)); return mkdir; } diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/MavenLocalRepoFileInitializer.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/MavenLocalRepoFileInitializer.java index a9f1c3c4e12..cbd8674dadd 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/MavenLocalRepoFileInitializer.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/MavenLocalRepoFileInitializer.java @@ -165,7 +165,7 @@ public class MavenLocalRepoFileInitializer extends DownloadFileInitializer if (!FS.canReadFile(localFile)) { if (FS.ensureDirectoryExists(localFile.getParent())) - StartLog.info("mkdir " + _basehome.toShortForm(localFile.getParent())); + StartLog.info("mkdir %s", _basehome.toShortForm(localFile.getParent())); download(coords, localFile); if (!FS.canReadFile(localFile)) { @@ -209,7 +209,7 @@ public class MavenLocalRepoFileInitializer extends DownloadFileInitializer if (localRepoFile != null) { if (FS.ensureDirectoryExists(destination.getParent())) - StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent())); + StartLog.info("mkdir %s", _basehome.toShortForm(destination.getParent())); StartLog.info("copy %s to %s", localRepoFile, _basehome.toShortForm(destination)); Files.copy(localRepoFile, destination); return true; diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java index 4e3fc47e195..148b3b3d6af 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java @@ -59,11 +59,16 @@ import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.RegisterExtension; import static org.junit.jupiter.api.Assertions.assertTrue; public class AbstractTest { + @RegisterExtension + public final BeforeTestExecutionCallback printMethodName = context -> + System.err.printf("Running %s.%s() %s%n", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), context.getDisplayName()); protected final HttpConfiguration httpConfig = new HttpConfiguration(); protected SslContextFactory.Server sslContextFactoryServer; protected Server server; diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java index a3b73d19dde..676f68bb0a7 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java @@ -22,7 +22,6 @@ import org.eclipse.jetty.client.Connection; import org.eclipse.jetty.client.Destination; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; -import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.transport.HttpExchange; import org.eclipse.jetty.client.transport.internal.HttpChannelOverHTTP; @@ -209,9 +208,9 @@ public class HttpChannelAssociationTest extends AbstractTest return new HttpConnectionOverFCGI(endPoint, destination, promise) { @Override - protected HttpChannelOverFCGI newHttpChannel(Request request) + protected HttpChannelOverFCGI newHttpChannel() { - return new HttpChannelOverFCGI(this, getFlusher(), request.getIdleTimeout()) + return new HttpChannelOverFCGI(this) { @Override public boolean associate(HttpExchange exchange) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java index 11ff341e555..22cb55ee241 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java @@ -478,8 +478,7 @@ public interface Callback extends Invocable } catch (Throwable t) { - if (ExceptionUtil.areNotAssociated(x, t)) - x.addSuppressed(t); + ExceptionUtil.addSuppressedIfNotAssociated(x, t); } finally { diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/CompletableTask.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/CompletableTask.java new file mode 100644 index 00000000000..4220698c96d --- /dev/null +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/CompletableTask.java @@ -0,0 +1,76 @@ +// +// ======================================================================== +// 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.util; + +import java.util.concurrent.CompletableFuture; + +/** + *

A {@link CompletableFuture} that implements {@link Runnable} to perform + * a one-shot task that eventually completes this {@link CompletableFuture}.

+ *

Subclasses override {@link #run()} to implement the task.

+ *

Users of this class start the task execution via {@link #start()}.

+ *

Typical usage:

+ *
{@code
+ * CompletableTask task = new CompletableTask<>()
+ * {
+ *     @Override
+ *     public void run()
+ *     {
+ *         try
+ *         {
+ *             // Perform some task.
+ *             T result = performTask();
+ *
+ *             // Eventually complete this CompletableFuture.
+ *             complete(result);
+ *         }
+ *         catch (Throwable x)
+ *         {
+ *             completeExceptionally(x);
+ *         }
+ *     }
+ * }
+ *
+ * // Start the task and then process the
+ * // result of the task when it is complete.
+ * task.start()
+ *     .whenComplete((result, failure) ->
+ *     {
+ *         if (failure == null)
+ *         {
+ *             // The task completed successfully.
+ *         }
+ *         else
+ *         {
+ *             // The task failed.
+ *         }
+ *     });
+ * }
+ * + * @param the type of the result of the task + */ +public abstract class CompletableTask extends CompletableFuture implements Runnable +{ + /** + *

Starts the task by calling {@link #run()} + * and returns this {@link CompletableTask}.

+ * + * @return this {@link CompletableTask} + */ + public CompletableTask start() + { + run(); + return this; + } +} diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java index 279e5136686..069e844da71 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.util.thread.Invocable; */ public class ExceptionUtil { + /** *

Convert a {@link Throwable} to a specific type by casting or construction on a new instance.

* @@ -178,6 +179,18 @@ public class ExceptionUtil return true; } + /** + * Add a suppressed exception if it is not associated. + * @see #areNotAssociated(Throwable, Throwable) + * @param throwable The main Throwable + * @param suppressed The Throwable to suppress if it is not associated. + */ + public static void addSuppressedIfNotAssociated(Throwable throwable, Throwable suppressed) + { + if (areNotAssociated(throwable, suppressed)) + throwable.addSuppressed(suppressed); + } + /** * Decorate a Throwable with the suppressed errors and return it. * @param t the throwable diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java index 36ff51491f2..c49049c38d9 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java @@ -1836,8 +1836,7 @@ public final class URIUtil Objects.requireNonNull(resource); // Only try URI for string for known schemes, otherwise assume it is a Path - ResourceFactory resourceFactory = ResourceFactory.getBestByScheme(resource); - return (resourceFactory != null) + return (ResourceFactory.isSupported(resource)) ? correctFileURI(URI.create(resource)) : Paths.get(resource).toUri(); } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java index 300d2335175..6927c09295e 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactory.java @@ -27,7 +27,89 @@ import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.Dumpable; /** - * ResourceFactory. + *

ResourceFactory is the source of new {@link Resource} instances.

+ * + *

+ * Some {@link Resource} objects have an internal allocation / release model, + * that the {@link ResourceFactory} is responsible for. + * Once a {@link ResourceFactory} is stopped, the {@link Resource} + * objects created from that {@link ResourceFactory} are released. + *

+ * + *

A {@link ResourceFactory.LifeCycle} tied to a Jetty {@link org.eclipse.jetty.util.component.Container}

+ *
+ *     ResourceFactory.LifeCycle resourceFactory = ResourceFactory.of(container);
+ *     Resource resource = resourceFactory.newResource(ref);
+ * 
+ *

+ * The use of {@link ResourceFactory#of(Container)} results in a {@link ResourceFactory.LifeCycle} that is tied + * to a specific Jetty {@link org.eclipse.jetty.util.component.Container} such as a {@code Server}, + * {@code ServletContextHandler}, or {@code WebAppContext}. This will free the {@code Resource} + * instances created by the {@link org.eclipse.jetty.util.resource.ResourceFactory} once + * the {@code container} that manages it is stopped. + *

+ * + *

A {@link ResourceFactory.Closeable} that exists within a {@code try-with-resources} call

+ *
+ *     try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable()) {
+ *         Resource resource = resourceFactory.newResource(ref);
+ *     }
+ * 
+ *

+ * The use of {@link ResourceFactory#closeable()} results in a {@link ResourceFactory} that only exists for + * the duration of the {@code try-with-resources} code block, once this {@code try-with-resources} is closed, + * all {@link Resource} objects associated with that {@link ResourceFactory} are freed as well. + *

+ * + *

A {@code ResourceFactory} that lives at the JVM level

+ *
+ *     ResourceFactory resourceFactory = ResourceFactory.root();
+ *     Resource resource = resourceFactory.newResource(ref);
+ * 
+ *

+ * The use of {@link ResourceFactory#root()} results in a {@link ResourceFactory} that exists for + * the life of the JVM, and the resources allocated via this {@link ResourceFactory} will not + * be freed until the JVM exits. + *

+ * + *

Supported URI Schemes

+ *

+ * By default, the following schemes are supported by Jetty. + *

+ *
+ *
file
+ *
The standard Java {@code file:/path/to/dir/} syntax
+ * + *
jar
+ *
The standard Java {@code jar:file:/path/to/file.jar!/} syntax
+ * + *
jrt
+ *
The standard Java {@code jrt:module-name} syntax
+ *
+ *

+ * Special Note: An effort is made to discover any new schemes that + * might be present at JVM startup (eg: graalvm and {@code resource:} scheme). + * At startup Jetty will access an internal Jetty resource (found in + * the jetty-util jar) and seeing what {@code scheme} it is using to access + * it, and will register it with a call to + * {@link ResourceFactory#registerResourceFactory(String, ResourceFactory)}. + *

+ * + *

Supporting more Schemes

+ *

+ * You can register a new URI scheme to a {@link ResourceFactory} implementation + * using the {@link ResourceFactory#registerResourceFactory(String, ResourceFactory)} + * method, which will cause all new uses of ResourceFactory to use this newly + * registered scheme. + *

+ *
+ *     URLResourceFactory urlResourceFactory = new URLResourceFactory();
+ *     urlResourceFactory.setConnectTimeout(1000);
+ *     ResourceFactory.registerResourceFactory("https", urlResourceFactory);
+ *
+ *     URI web = URI.create("https://eclipse.dev/jetty/");
+ *     Resource resource = ResourceFactory.root().newResource(web);
+ * 
*/ public interface ResourceFactory { @@ -39,7 +121,6 @@ public interface ResourceFactory * or null if none are passed. * The returned {@link Resource} will always return {@code true} from {@link Resource#isDirectory()} * @throws IllegalArgumentException if a non-directory resource is passed. - * @see CombinedResource */ static Resource combine(List resources) { @@ -54,7 +135,6 @@ public interface ResourceFactory * or null if none are passed. * The returned {@link Resource} will always return {@code true} from {@link Resource#isDirectory()} * @throws IllegalArgumentException if a non-directory resource is passed. - * @see CombinedResource */ static Resource combine(Resource... resources) { @@ -65,17 +145,21 @@ public interface ResourceFactory * Construct a resource from a uri. * * @param uri A URI. - * @return A Resource object. + * @return A Resource object, or null if uri points to a location that does not exist. */ Resource newResource(URI uri); /** - * Construct a system resource from a string. - * The resource is tried as classloader resource before being - * treated as a normal resource. + *

Construct a system resource from a string.

+ * + *

+ * The resource is first attempted to be accessed via the {@link Thread#getContextClassLoader()} + * before being treated as a normal resource. + *

* * @param resource Resource as string representation - * @return The new Resource + * @return The new Resource, or null if string points to a location that does not exist + * @throws IllegalArgumentException if string is blank */ default Resource newSystemResource(String resource) { @@ -134,12 +218,16 @@ public interface ResourceFactory } /** - * Find a classpath resource. + *

Find a classpath resource.

+ * + *

* The {@link Class#getResource(String)} method is used to lookup the resource. If it is not * found, then the {@link Loader#getResource(String)} method is used. + *

* * @param resource the relative name of the resource - * @return Resource or null + * @return Resource, or null if string points to a location that does not exist + * @throws IllegalArgumentException if string is blank */ default Resource newClassPathResource(String resource) { @@ -164,9 +252,20 @@ public interface ResourceFactory } /** + *

* Load a URL into a memory resource. + *

+ * + *

+ * A Memory Resource is created from a the contents of + * {@link URL#openStream()} and kept in memory from + * that point forward. Never accessing the URL + * again to refresh it's contents. + *

+ * * @param url the URL to load into memory - * @return Resource or null + * @return Resource, or null if url points to a location that does not exist + * @throws IllegalArgumentException if URL is null * @see #newClassPathResource(String) */ default Resource newMemoryResource(URL url) @@ -181,7 +280,9 @@ public interface ResourceFactory * Construct a resource from a string. * * @param resource A URL or filename. - * @return A Resource object. + * @return A Resource object, or null if the string points to a location + * that does not exist + * @throws IllegalArgumentException if resource is invalid */ default Resource newResource(String resource) { @@ -192,10 +293,12 @@ public interface ResourceFactory } /** - * Construct a Resource from provided path + * Construct a Resource from provided path. * * @param path the path - * @return the Resource for the provided path + * @return the Resource for the provided path, or null if the path + * does not exist + * @throws IllegalArgumentException if path is null */ default Resource newResource(Path path) { @@ -206,10 +309,12 @@ public interface ResourceFactory } /** - * Construct a possible {@link CombinedResource} from a list of URIs + * Construct a possible combined {@code Resource} from a list of URIs. * * @param uris the URIs - * @return the Resource for the provided path + * @return the Resource for the provided URIs, or null if all + * of the provided URIs do not exist + * @throws IllegalArgumentException if list of URIs is empty or null */ default Resource newResource(List uris) { @@ -220,10 +325,12 @@ public interface ResourceFactory } /** - * Construct a {@link Resource} from a provided URL + * Construct a {@link Resource} from a provided URL. * * @param url the URL - * @return the Resource for the provided URL + * @return the Resource for the provided URL, or null if the + * url points to a location that does not exist + * @throws IllegalArgumentException if url is null */ default Resource newResource(URL url) { @@ -241,10 +348,12 @@ public interface ResourceFactory } /** - * Construct a {@link Resource} from a {@code file:} based URI that is mountable (eg: a jar file) + * Construct a {@link Resource} from a {@code file:} based URI that is mountable (eg: a jar file). * * @param uri the URI - * @return the Resource, mounted as a {@link java.nio.file.FileSystem} + * @return the Resource, mounted as a {@link java.nio.file.FileSystem}, or null if + * the uri points to a location that does not exist. + * @throws IllegalArgumentException if provided URI is not "file" scheme. */ default Resource newJarFileResource(URI uri) { @@ -253,6 +362,28 @@ public interface ResourceFactory return newResource(URIUtil.toJarFileUri(uri)); } + /** + * Test to see if provided string is supported. + * + * @param str the string to test + * @return true if it is supported + */ + static boolean isSupported(String str) + { + return ResourceFactoryInternals.isSupported(str); + } + + /** + * Test to see if provided uri is supported. + * + * @param uri the uri to test + * @return true if it is supported + */ + static boolean isSupported(URI uri) + { + return ResourceFactoryInternals.isSupported(uri); + } + /** * Register a new ResourceFactory that can handle the specific scheme for the Resource API. * @@ -262,12 +393,14 @@ public interface ResourceFactory * * @param scheme the scheme to support (eg: `ftp`, `http`, `resource`, etc) * @param resourceFactory the ResourceFactory to be responsible for the registered scheme. + * @throws IllegalArgumentException if scheme is blank * @see #unregisterResourceFactory(String) - * @see #byScheme(String) - * @see #getBestByScheme(String) */ static void registerResourceFactory(String scheme, ResourceFactory resourceFactory) { + if (StringUtil.isBlank(scheme)) + throw new IllegalArgumentException("Scheme is blank"); + ResourceFactoryInternals.RESOURCE_FACTORIES.put(scheme, resourceFactory); } @@ -276,53 +409,16 @@ public interface ResourceFactory * * @param scheme the scheme to unregister * @return the existing {@link ResourceFactory} that was registered to that scheme. + * @throws IllegalArgumentException if scheme is blank * @see #registerResourceFactory(String, ResourceFactory) - * @see #byScheme(String) - * @see #getBestByScheme(String) */ static ResourceFactory unregisterResourceFactory(String scheme) { + if (StringUtil.isBlank(scheme)) + throw new IllegalArgumentException("Scheme is blank"); return ResourceFactoryInternals.RESOURCE_FACTORIES.remove(scheme); } - /** - * Get the {@link ResourceFactory} that is registered for the specific scheme. - * - *
{@code
-     * .byScheme("jar") == ResourceFactory supporting jar
-     * .byScheme("jar:file://foo.jar!/") == null // full url strings not supported)
-     * }
- * - * @param scheme the scheme to look up - * @return the {@link ResourceFactory} responsible for the scheme, null if no {@link ResourceFactory} handles the scheme. - * @see #registerResourceFactory(String, ResourceFactory) - * @see #unregisterResourceFactory(String) - * @see #getBestByScheme(String) - */ - static ResourceFactory byScheme(String scheme) - { - return ResourceFactoryInternals.RESOURCE_FACTORIES.get(scheme); - } - - /** - * Get the best ResourceFactory for the provided scheme. - * - *

- * Unlike {@link #byScheme(String)}, this supports arbitrary Strings, that might start with a supported scheme. - *

- * - * @param scheme the scheme to look up - * @return the ResourceFactory that best fits the provided scheme. - * @see org.eclipse.jetty.util.Index#getBest(String) - * @see #registerResourceFactory(String, ResourceFactory) - * @see #unregisterResourceFactory(String) - * @see #byScheme(String) - */ - static ResourceFactory getBestByScheme(String scheme) - { - return ResourceFactoryInternals.RESOURCE_FACTORIES.getBest(scheme); - } - /** * The JVM wide (root) ResourceFactory. * diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java index 595ca615feb..1bc7728382f 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java @@ -24,6 +24,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.Index; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.Dumpable; @@ -93,6 +94,39 @@ class ResourceFactoryInternals } }; + /** + * Test uri to know if a {@link ResourceFactory} is registered for it. + * + * @param uri the uri to test + * @return true if a ResourceFactory is registered to support the uri + * @see ResourceFactory#registerResourceFactory(String, ResourceFactory) + * @see ResourceFactory#unregisterResourceFactory(String) + * @see #isSupported(String) + */ + static boolean isSupported(URI uri) // TODO: boolean isSupported + { + if (uri == null || uri.getScheme() == null) + return false; + return RESOURCE_FACTORIES.get(uri.getScheme()) != null; + } + + /** + * Test string to know if a {@link ResourceFactory} is registered for it. + * + * @param str the string representing the resource location + * @return true if a ResourceFactory is registered to support the string representation + * @see org.eclipse.jetty.util.Index#getBest(String) + * @see ResourceFactory#registerResourceFactory(String, ResourceFactory) + * @see ResourceFactory#unregisterResourceFactory(String) + * @see #isSupported(URI) + */ + static boolean isSupported(String str) + { + if (StringUtil.isBlank(str)) + return false; + return RESOURCE_FACTORIES.getBest(str) != null; + } + static class Closeable implements ResourceFactory.Closeable { private final CompositeResourceFactory _compositeResourceFactory = new CompositeResourceFactory(); @@ -168,7 +202,7 @@ class ResourceFactoryInternals uri = URIUtil.correctFileURI(uri); } - ResourceFactory resourceFactory = ResourceFactory.byScheme(uri.getScheme()); + ResourceFactory resourceFactory = RESOURCE_FACTORIES.get(uri.getScheme()); if (resourceFactory == null) throw new IllegalArgumentException("URI scheme not supported: " + uri); if (resourceFactory instanceof MountedPathResourceFactory) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/package-info.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/package-info.java index 9102cdf6abb..c1fe89fd4c3 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/package-info.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/package-info.java @@ -12,7 +12,17 @@ // /** - * Jetty Util : Common Resource Utilities + *

Jetty Util : Resource Utilities

+ * + *

+ * A {@link org.eclipse.jetty.util.resource.Resource} in Jetty is an abstraction that + * allows for a common API to access various forms of resource sources across the Jetty + * ecosystem. + *

+ *

+ * A {@link org.eclipse.jetty.util.resource.Resource} is created via one of the + * {@link org.eclipse.jetty.util.resource.ResourceFactory}{@code .newResource(...)} APIs. + *

*/ package org.eclipse.jetty.util.resource; 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 d3c6d8aecf9..cc2513a31c7 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 @@ -149,11 +149,6 @@ public class AsyncContextState implements AsyncContext _state = null; } - public ServletChannelState getServletChannelState() - { - return state(); - } - public static class WrappedAsyncListener implements AsyncListener { private final AsyncListener _listener; 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 729e84e17ad..ff8ba93a069 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 @@ -15,26 +15,17 @@ package org.eclipse.jetty.ee10.servlet; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStreamReader; import java.net.URI; -import java.nio.ByteBuffer; import java.nio.file.InvalidPathException; import java.time.Duration; import java.util.ArrayList; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashSet; import java.util.List; -import java.util.ListIterator; import java.util.Locale; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.StringTokenizer; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; -import java.util.stream.Collectors; +import jakarta.servlet.AsyncContext; import jakarta.servlet.DispatcherType; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletContext; @@ -43,16 +34,12 @@ import jakarta.servlet.UnavailableException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpServletResponseWrapper; import jakarta.servlet.http.MappingMatch; import org.eclipse.jetty.http.CompressedContentFormat; import org.eclipse.jetty.http.HttpException; 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.http.HttpStatus; -import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.content.FileMappingHttpContentFactory; import org.eclipse.jetty.http.content.HttpContent; @@ -60,7 +47,6 @@ import org.eclipse.jetty.http.content.PreCompressedHttpContentFactory; import org.eclipse.jetty.http.content.ResourceHttpContentFactory; import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory; import org.eclipse.jetty.http.content.VirtualHttpContentFactory; -import org.eclipse.jetty.io.ByteBufferInputStream; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.Request; @@ -69,9 +55,8 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.Blocker; -import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.ExceptionUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; @@ -79,6 +64,8 @@ import org.eclipse.jetty.util.resource.Resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.eclipse.jetty.util.URIUtil.encodePath; + /** *

The default Servlet, normally mapped to {@code /}, that handles static resources.

*

The following init parameters are supported:

@@ -185,7 +172,6 @@ public class DefaultServlet extends HttpServlet private ServletContextHandler _contextHandler; private ServletResourceService _resourceService; private WelcomeServletMode _welcomeServletMode; - private Resource _baseResource; public ResourceService getResourceService() { @@ -198,14 +184,14 @@ public class DefaultServlet extends HttpServlet _contextHandler = initContextHandler(getServletContext()); _resourceService = new ServletResourceService(_contextHandler); _resourceService.setWelcomeFactory(_resourceService); - _baseResource = _contextHandler.getBaseResource(); + Resource baseResource = _contextHandler.getBaseResource(); String rb = getInitParameter("baseResource", "resourceBase"); if (rb != null) { try { - _baseResource = Objects.requireNonNull(_contextHandler.newResource(rb)); + baseResource = Objects.requireNonNull(_contextHandler.newResource(rb)); } catch (Exception e) { @@ -222,7 +208,7 @@ public class DefaultServlet extends HttpServlet if (contentFactory == null) { MimeTypes mimeTypes = _contextHandler.getMimeTypes(); - ResourceFactory resourceFactory = _baseResource != null ? ResourceFactory.of(_baseResource) : this::getResource; + ResourceFactory resourceFactory = baseResource != null ? ResourceFactory.of(baseResource) : this::getResource; contentFactory = new ResourceHttpContentFactory(resourceFactory, mimeTypes); // Use the servers default stylesheet unless there is one explicitly set by an init param. @@ -326,7 +312,7 @@ public class DefaultServlet extends HttpServlet if (LOG.isDebugEnabled()) { - LOG.debug(" .baseResource = {}", _baseResource); + LOG.debug(" .baseResource = {}", baseResource); LOG.debug(" .resourceService = {}", _resourceService); LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode); } @@ -458,18 +444,18 @@ public class DefaultServlet extends HttpServlet } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { - String includedServletPath = (String)req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); - String encodedPathInContext = getEncodedPathInContext(req, includedServletPath); + String includedServletPath = (String)httpServletRequest.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + String encodedPathInContext = getEncodedPathInContext(httpServletRequest, includedServletPath); boolean included = includedServletPath != null; if (LOG.isDebugEnabled()) - LOG.debug("doGet(req={}, resp={}) pathInContext={}, included={}", req, resp, encodedPathInContext, included); + LOG.debug("doGet(hsReq={}, hsResp={}) pathInContext={}, included={}", httpServletRequest, httpServletResponse, encodedPathInContext, included); try { - HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(req)); + HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(httpServletRequest)); if (LOG.isDebugEnabled()) LOG.debug("content = {}", content); @@ -487,13 +473,31 @@ public class DefaultServlet extends HttpServlet } // no content - resp.sendError(404); + httpServletResponse.sendError(404); } else { - ServletCoreRequest coreRequest = new ServletCoreRequest(req); - ServletCoreResponse coreResponse = new ServletCoreResponse(coreRequest, resp, included); + // lookup the core request and response as wrapped by the ServletContextHandler + ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(httpServletRequest); + ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse(); + ServletChannel servletChannel = servletContextRequest.getServletChannel(); + // If the servlet request has not been wrapped, + // we can use the core request directly, + // otherwise wrap the servlet request as a core request + Request coreRequest = httpServletRequest instanceof ServletApiRequest + ? servletChannel.getRequest() + : new ServletCoreRequest(httpServletRequest); + + // If the servlet response has been wrapped and has been written to, + // then the servlet response must be wrapped as a core response + // otherwise we can use the core response directly. + boolean useServletResponse = !(httpServletResponse instanceof ServletApiResponse) || servletContextResponse.isWritingOrStreaming(); + Response coreResponse = useServletResponse + ? new ServletCoreResponse(coreRequest, httpServletResponse, included) + : servletChannel.getResponse(); + + // If the core response is already committed then do nothing more if (coreResponse.isCommitted()) { if (LOG.isDebugEnabled()) @@ -501,30 +505,39 @@ public class DefaultServlet extends HttpServlet return; } + // Get the content length before we may wrap the content + long contentLength = content.getContentLengthValue(); + // Servlet Filters could be interacting with the Response already. - if (coreResponse.isHttpServletResponseWrapped() || - coreResponse.isWritingOrStreaming()) - { + if (useServletResponse) content = new UnknownLengthHttpContent(content); - } - ServletContextResponse contextResponse = coreResponse.getServletContextResponse(); - if (contextResponse != null) - { - String characterEncoding = contextResponse.getRawCharacterEncoding(); - if (characterEncoding != null) - content = new ForcedCharacterEncodingHttpContent(content, characterEncoding); - } + // The character encoding may be forced + String characterEncoding = servletContextResponse.getRawCharacterEncoding(); + if (characterEncoding != null) + content = new ForcedCharacterEncodingHttpContent(content, characterEncoding); - // serve content - try (Blocker.Callback callback = Blocker.callback()) + // If async is supported and the unwrapped content is larger than an output buffer + if (httpServletRequest.isAsyncSupported() && + (contentLength < 0 || contentLength > coreRequest.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize())) { + // send the content asynchronously + AsyncContext asyncContext = httpServletRequest.startAsync(); + Callback callback = new AsyncContextCallback(asyncContext, httpServletResponse); _resourceService.doGet(coreRequest, coreResponse, callback, content); - callback.block(); } - catch (Exception e) + else { - throw new ServletException(e); + // send the content blocking + try (Blocker.Callback callback = Blocker.callback()) + { + _resourceService.doGet(coreRequest, coreResponse, callback, content); + callback.block(); + } + catch (Exception e) + { + throw new ServletException(e); + } } } } @@ -534,16 +547,16 @@ public class DefaultServlet extends HttpServlet LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e); if (included) throw new FileNotFoundException(encodedPathInContext); - resp.setStatus(404); + httpServletResponse.setStatus(404); } } protected String getEncodedPathInContext(HttpServletRequest req, String includedServletPath) { if (includedServletPath != null) - return URIUtil.encodePath(getIncludedPathInContext(req, includedServletPath, !isDefaultMapping(req))); + return encodePath(getIncludedPathInContext(req, includedServletPath, !isDefaultMapping(req))); else if (!isDefaultMapping(req)) - return URIUtil.encodePath(req.getPathInfo()); + return encodePath(req.getPathInfo()); else if (req instanceof ServletApiRequest apiRequest) return Context.getPathInContext(req.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath()); else @@ -589,476 +602,6 @@ public class DefaultServlet extends HttpServlet return result; } - private static class ServletCoreRequest extends Request.Wrapper - { - // TODO fully implement this class and move it to the top level - // TODO Some methods are directed to core that probably should be intercepted - - private final HttpServletRequest _servletRequest; - private final HttpFields _httpFields; - private final HttpURI _uri; - - ServletCoreRequest(HttpServletRequest request) - { - super(ServletContextRequest.getServletContextRequest(request)); - _servletRequest = request; - - HttpFields.Mutable fields = HttpFields.build(); - - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) - { - String headerName = headerNames.nextElement(); - Enumeration headerValues = request.getHeaders(headerName); - while (headerValues.hasMoreElements()) - { - String headerValue = headerValues.nextElement(); - fields.add(new HttpField(headerName, headerValue)); - } - } - - _httpFields = fields.asImmutable(); - String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); - boolean included = includedServletPath != null; - if (request.getDispatcherType() == DispatcherType.REQUEST) - _uri = getWrapped().getHttpURI(); - else if (included) - _uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(getIncludedPathInContext(request, includedServletPath, false))); - else - _uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))); - } - - @Override - public HttpFields getHeaders() - { - return _httpFields; - } - - @Override - public HttpURI getHttpURI() - { - return _uri; - } - - @Override - public String getId() - { - return _servletRequest.getRequestId(); - } - - @Override - public String getMethod() - { - return _servletRequest.getMethod(); - } - - @Override - public boolean isSecure() - { - return _servletRequest.isSecure(); - } - - @Override - public Object removeAttribute(String name) - { - Object value = _servletRequest.getAttribute(name); - _servletRequest.removeAttribute(name); - return value; - } - - @Override - public Object setAttribute(String name, Object attribute) - { - Object value = _servletRequest.getAttribute(name); - _servletRequest.setAttribute(name, attribute); - return value; - } - - @Override - public Object getAttribute(String name) - { - return _servletRequest.getAttribute(name); - } - - @Override - public Set getAttributeNameSet() - { - Set set = new HashSet<>(); - Enumeration e = _servletRequest.getAttributeNames(); - while (e.hasMoreElements()) - set.add(e.nextElement()); - return set; - } - - @Override - public void clearAttributes() - { - Enumeration e = _servletRequest.getAttributeNames(); - while (e.hasMoreElements()) - _servletRequest.removeAttribute(e.nextElement()); - } - } - - private static class HttpServletResponseHttpFields implements HttpFields.Mutable - { - private final HttpServletResponse _response; - - private HttpServletResponseHttpFields(HttpServletResponse response) - { - _response = response; - } - - @Override - public ListIterator listIterator() - { - // The minimum requirement is to implement the listIterator, but it is inefficient. - // Other methods are implemented for efficiency. - final ListIterator list = _response.getHeaderNames().stream() - .map(n -> new HttpField(n, _response.getHeader(n))) - .collect(Collectors.toList()) - .listIterator(); - - return new ListIterator<>() - { - HttpField _last; - - @Override - public boolean hasNext() - { - return list.hasNext(); - } - - @Override - public HttpField next() - { - return _last = list.next(); - } - - @Override - public boolean hasPrevious() - { - return list.hasPrevious(); - } - - @Override - public HttpField previous() - { - return _last = list.previous(); - } - - @Override - public int nextIndex() - { - return list.nextIndex(); - } - - @Override - public int previousIndex() - { - return list.previousIndex(); - } - - @Override - public void remove() - { - if (_last != null) - { - // This is not exactly the right semantic for repeated field names - list.remove(); - _response.setHeader(_last.getName(), null); - } - } - - @Override - public void set(HttpField httpField) - { - list.set(httpField); - _response.setHeader(httpField.getName(), httpField.getValue()); - } - - @Override - public void add(HttpField httpField) - { - list.add(httpField); - _response.addHeader(httpField.getName(), httpField.getValue()); - } - }; - } - - @Override - public Mutable add(String name, String value) - { - _response.addHeader(name, value); - return this; - } - - @Override - public Mutable add(HttpHeader header, HttpHeaderValue value) - { - _response.addHeader(header.asString(), value.asString()); - return this; - } - - @Override - public Mutable add(HttpHeader header, String value) - { - _response.addHeader(header.asString(), value); - return this; - } - - @Override - public Mutable add(HttpField field) - { - _response.addHeader(field.getName(), field.getValue()); - return this; - } - - @Override - public Mutable put(HttpField field) - { - _response.setHeader(field.getName(), field.getValue()); - return this; - } - - @Override - public Mutable put(String name, String value) - { - _response.setHeader(name, value); - return this; - } - - @Override - public Mutable put(HttpHeader header, HttpHeaderValue value) - { - _response.setHeader(header.asString(), value.asString()); - return this; - } - - @Override - public Mutable put(HttpHeader header, String value) - { - _response.setHeader(header.asString(), value); - return this; - } - - @Override - public Mutable put(String name, List list) - { - Objects.requireNonNull(name); - Objects.requireNonNull(list); - boolean first = true; - for (String s : list) - { - if (first) - _response.setHeader(name, s); - else - _response.addHeader(name, s); - first = false; - } - return this; - } - - @Override - public Mutable remove(HttpHeader header) - { - _response.setHeader(header.asString(), null); - return this; - } - - @Override - public Mutable remove(EnumSet fields) - { - for (HttpHeader header : fields) - remove(header); - return this; - } - - @Override - public Mutable remove(String name) - { - _response.setHeader(name, null); - return this; - } - } - - private static class ServletCoreResponse implements Response - { - // TODO fully implement this class and move it to the top level - - private final HttpServletResponse _response; - private final ServletCoreRequest _coreRequest; - private final Response _coreResponse; - private final HttpFields.Mutable _httpFields; - private final boolean _included; - - public ServletCoreResponse(ServletCoreRequest coreRequest, HttpServletResponse response, boolean included) - { - _coreRequest = coreRequest; - _response = response; - _coreResponse = ServletContextResponse.getServletContextResponse(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 - public HttpFields.Mutable getHeaders() - { - return _httpFields; - } - - public ServletContextResponse getServletContextResponse() - { - return ServletContextResponse.getServletContextResponse(_response); - } - - @Override - public boolean isCommitted() - { - return _response.isCommitted(); - } - - /** - * Test if the HttpServletResponse is wrapped by the webapp. - * - * @return true if wrapped. - */ - public boolean isHttpServletResponseWrapped() - { - return (_response instanceof HttpServletResponseWrapper); - } - - /** - * Test if {@link HttpServletResponse#getOutputStream()} or - * {@link HttpServletResponse#getWriter()} has been called already - * - * @return true if {@link HttpServletResponse} has started to write or stream content - */ - public boolean isWritingOrStreaming() - { - ServletContextResponse servletContextResponse = Response.as(_coreResponse, ServletContextResponse.class); - return servletContextResponse.isWritingOrStreaming(); - } - - public boolean isWriting() - { - ServletContextResponse servletContextResponse = Response.as(_coreResponse, ServletContextResponse.class); - return servletContextResponse.isWriting(); - } - - @Override - public void write(boolean last, ByteBuffer byteBuffer, Callback callback) - { - if (_included) - last = false; - try - { - if (BufferUtil.hasContent(byteBuffer)) - { - if (isWriting()) - { - String characterEncoding = _response.getCharacterEncoding(); - try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer); - InputStreamReader reader = new InputStreamReader(bbis, characterEncoding)) - { - IO.copy(reader, _response.getWriter()); - } - - if (last) - _response.getWriter().close(); - } - else - { - BufferUtil.writeTo(byteBuffer, _response.getOutputStream()); - if (last) - _response.getOutputStream().close(); - } - } - - callback.succeeded(); - } - catch (Throwable t) - { - callback.failed(t); - } - } - - @Override - public Request getRequest() - { - return _coreRequest; - } - - @Override - public int getStatus() - { - return _response.getStatus(); - } - - @Override - public void setStatus(int code) - { - if (LOG.isDebugEnabled()) - LOG.debug("{}.setStatus({})", this.getClass().getSimpleName(), code); - if (_included) - return; - _response.setStatus(code); - } - - @Override - public Supplier getTrailersSupplier() - { - return null; - } - - @Override - public void setTrailersSupplier(Supplier trailers) - { - } - - @Override - public boolean isCompletedSuccessfully() - { - return _coreResponse.isCompletedSuccessfully(); - } - - @Override - public void reset() - { - _response.reset(); - } - - @Override - public CompletableFuture writeInterim(int status, HttpFields headers) - { - return null; - } - - @Override - public String toString() - { - return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _response); - } - } - private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory { private final ServletContextHandler _servletContextHandler; @@ -1214,18 +757,32 @@ public class DefaultServlet extends HttpServlet private HttpServletRequest getServletRequest(Request request) { - // TODO, this unwrapping is fragile - return ((ServletCoreRequest)request)._servletRequest; + ServletCoreRequest servletCoreRequest = Request.as(request, ServletCoreRequest.class); + if (servletCoreRequest != null) + return servletCoreRequest.getServletRequest(); + + ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class); + if (servletContextRequest != null) + return servletContextRequest.getServletApiRequest(); + + throw new IllegalStateException("instanceof " + request.getClass()); } private HttpServletResponse getServletResponse(Response response) { - // TODO, this unwrapping is fragile - return ((ServletCoreResponse)response)._response; + ServletCoreResponse servletCoreResponse = Response.as(response, ServletCoreResponse.class); + if (servletCoreResponse != null) + return servletCoreResponse.getServletResponse(); + + ServletContextResponse servletContextResponse = Response.as(response, ServletContextResponse.class); + if (servletContextResponse != null) + return servletContextResponse.getServletApiResponse(); + + throw new IllegalStateException("instanceof " + response.getClass()); } } - private static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly) + static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly) { String servletPath = isPathInfoOnly ? "/" : includedServletPath; String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); @@ -1330,4 +887,45 @@ public class DefaultServlet extends HttpServlet */ EXACT } + + private static class AsyncContextCallback implements Callback + { + private final AsyncContext _asyncContext; + private final HttpServletResponse _response; + + private AsyncContextCallback(AsyncContext asyncContext, HttpServletResponse response) + { + _asyncContext = asyncContext; + _response = response; + } + + @Override + public void succeeded() + { + _asyncContext.complete(); + } + + @Override + public void failed(Throwable x) + { + try + { + if (LOG.isDebugEnabled()) + LOG.debug("AsyncContextCallback failed {}", _asyncContext, x); + // It is known that this callback is only failed if the response is already committed, + // thus we can only abort the response here. + _response.sendError(-1); + } + catch (IOException e) + { + ExceptionUtil.addSuppressedIfNotAssociated(x, e); + } + finally + { + _asyncContext.complete(); + } + if (LOG.isDebugEnabled()) + LOG.debug("Async get failed", x); + } + } } 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 767dc2a657a..4298fa36e5f 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 @@ -13,6 +13,7 @@ package org.eclipse.jetty.ee10.servlet; +import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -41,6 +42,7 @@ import org.eclipse.jetty.ee10.servlet.util.ServletOutputStreamWrapper; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.pathmap.MatchedResource; import org.eclipse.jetty.io.WriterOutputStream; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.UrlEncoded; @@ -122,16 +124,18 @@ public class Dispatcher implements RequestDispatcher _mappedServlet.handle(_servletHandler, _decodedPathInContext, new ForwardRequest(httpRequest), httpResponse); // If we are not async and not closed already, then close via the possibly wrapped response. - if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getHttpOutput().isClosed()) + if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getServletContextResponse().hasLastWrite()) { + Closeable closeable; try { - response.getOutputStream().close(); + closeable = response.getOutputStream(); } catch (IllegalStateException e) { - response.getWriter().close(); + closeable = response.getWriter(); } + IO.close(closeable); } } @@ -624,6 +628,24 @@ public class Dispatcher implements RequestDispatcher { // NOOP for include. } + + @Override + public void sendError(int sc, String msg) throws IOException + { + // NOOP for include. + } + + @Override + public void sendError(int sc) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location) throws IOException + { + // NOOP for include. + } } private class AsyncRequest extends ParameterRequestWrapper 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 365d02605c9..d57452af7ee 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 @@ -36,6 +36,7 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.io.RetainableByteBuffer; 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; @@ -154,12 +155,20 @@ public class HttpOutput extends ServletOutputStream implements Runnable _commitSize = _bufferSize; } } - + + /** + * @return True if any content has been written via the {@link jakarta.servlet.http.HttpServletResponse} API. + */ public boolean isWritten() { return _written > 0; } + /** + * @return The bytes written via the {@link jakarta.servlet.http.HttpServletResponse} API. This + * may differ from the bytes reported by {@link org.eclipse.jetty.server.Response#getContentBytesWritten(Response)} + * due to buffering, compression, other interception or writes that bypass the servlet API. + */ public long getWritten() { return _written; @@ -445,6 +454,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable Blocker.Callback blocker = null; try (AutoLock l = _channelState.lock()) { + if (_softClose) + return; + if (_onError != null) { if (_onError instanceof IOException) @@ -1456,8 +1468,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable } catch (Throwable t) { - if (ExceptionUtil.areNotAssociated(e, t)) - e.addSuppressed(t); + ExceptionUtil.addSuppressedIfNotAssociated(e, t); } finally { diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/NoJspServlet.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/NoJspServlet.java index 05dda4f5992..c23dea5d190 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/NoJspServlet.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/NoJspServlet.java @@ -28,7 +28,7 @@ public class NoJspServlet extends HttpServlet protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException { if (!_warned) - getServletContext().log("No JSP support. Check that JSP jars are in lib/jsp and that the JSP option has been specified to start.jar"); + getServletContext().log("No JSP support. Check that the ee10-jsp module is enabled, or otherwise ensure the jsp jars are on the server classpath."); _warned = true; response.sendError(500, "JSP support not configured"); 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 47a6d62f512..59c85855758 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 @@ -100,7 +100,6 @@ public class ServletApiRequest implements HttpServletRequest private static final Logger LOG = LoggerFactory.getLogger(ServletApiRequest.class); private final ServletContextRequest _servletContextRequest; private final ServletChannel _servletChannel; - //TODO review which fields should be in ServletContextRequest private AsyncContextState _async; private String _characterEncoding; private int _inputState = ServletContextRequest.INPUT_NONE; @@ -413,7 +412,7 @@ public class ServletApiRequest implements HttpServletRequest public boolean isRequestedSessionIdValid() { AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession(); - return requestedSession != null && requestedSession.sessionId() != null && !requestedSession.sessionIdFromCookie(); + return requestedSession != null && requestedSession.sessionId() != null && requestedSession.session() != null; } @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 b0c1b4b02ad..5715e5f6efb 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 @@ -88,7 +88,6 @@ public class ServletChannel private Response _response; private Callback _callback; private boolean _expects100Continue; - private long _written; public ServletChannel(ServletContextHandler servletContextHandler, Request request) { @@ -218,7 +217,7 @@ public class ServletChannel public long getBytesWritten() { - return _written; + return Response.getContentBytesWritten(getServletContextResponse()); } /** @@ -449,15 +448,13 @@ public class ServletChannel _request = _servletContextRequest = null; _response = null; _callback = null; - _written = 0; _expects100Continue = false; } /** * 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() + public void handle() { if (LOG.isDebugEnabled()) LOG.debug("handle {} {} ", _servletContextRequest.getHttpURI(), this); @@ -553,15 +550,15 @@ public class ServletChannel // If the callback has already been completed we should continue in handle loop. // Otherwise, the callback will schedule a dispatch to handle(). if (asyncCompletion.compareAndSet(false, true)) - return false; + return; } } catch (Throwable x) { if (cause == null) cause = x; - else if (ExceptionUtil.areNotAssociated(cause, x)) - cause.addSuppressed(x); + else + ExceptionUtil.addSuppressedIfNotAssociated(cause, x); if (LOG.isDebugEnabled()) LOG.debug("Could not perform ERROR dispatch, aborting", cause); if (_state.isResponseCommitted()) @@ -577,8 +574,7 @@ public class ServletChannel } catch (Throwable t) { - if (ExceptionUtil.areNotAssociated(cause, t)) - cause.addSuppressed(t); + ExceptionUtil.addSuppressedIfNotAssociated(cause, t); abort(cause); } } @@ -617,12 +613,11 @@ public class ServletChannel ResponseUtils.ensureConsumeAvailableOrNotPersistent(_servletContextRequest, _servletContextRequest.getServletContextResponse()); } - // RFC 7230, section 3.3. - if (!_servletContextRequest.isHead() && - getServletContextResponse().getStatus() != HttpStatus.NOT_MODIFIED_304 && - getServletContextResponse().isContentIncomplete(_servletContextRequest.getHttpOutput().getWritten())) + // RFC 7230, section 3.3. We do this here so that a servlet error page can be sent. + if (!_servletContextRequest.isHead() && getServletContextResponse().getStatus() != HttpStatus.NOT_MODIFIED_304) { - if (sendErrorOrAbort("Insufficient content written")) + long written = getBytesWritten(); + if (getServletContextResponse().isContentIncomplete(written) && sendErrorOrAbort("Insufficient content written %d < %d".formatted(written, getServletContextResponse().getContentLength()))) break; } @@ -649,9 +644,6 @@ public class ServletChannel if (LOG.isDebugEnabled()) LOG.debug("!handle {} {}", action, this); - - boolean suspended = action == Action.WAIT; - return !suspended; } private void reopen() @@ -664,7 +656,7 @@ public class ServletChannel * @param message the error message. * @return true if we have sent an error, false if we have aborted. */ - public boolean sendErrorOrAbort(String message) + private boolean sendErrorOrAbort(String message) { try { diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java index 0045a24566c..45362875bba 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java @@ -41,7 +41,7 @@ import static jakarta.servlet.RequestDispatcher.ERROR_SERVLET_NAME; import static jakarta.servlet.RequestDispatcher.ERROR_STATUS_CODE; /** - * Implementation of AsyncContext interface that holds the state of request-response cycle. + * holder of the state of request-response cycle. */ public class ServletChannelState { diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreRequest.java new file mode 100644 index 00000000000..a2822b1e002 --- /dev/null +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreRequest.java @@ -0,0 +1,261 @@ +// +// ======================================================================== +// 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.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Components; +import org.eclipse.jetty.server.ConnectionMetaData; +import org.eclipse.jetty.server.Context; +import org.eclipse.jetty.server.HttpStream; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; +import org.eclipse.jetty.server.TunnelSupport; +import org.eclipse.jetty.util.URIUtil; + +import static org.eclipse.jetty.util.URIUtil.addEncodedPaths; +import static org.eclipse.jetty.util.URIUtil.encodePath; + +/** + * Wrap a {@link jakarta.servlet.ServletRequest} as a core {@link Request}. + *

+ * Whilst similar to a {@link Request.Wrapper}, this class is not a {@code Wrapper} + * as callers should not be able to access {@link Wrapper#getWrapped()} and bypass + * the {@link jakarta.servlet.ServletRequest}. + *

+ *

+ * The current implementation does not support any read operations. + *

+ */ +class ServletCoreRequest implements Request +{ + private final HttpServletRequest _servletRequest; + private final ServletContextRequest _servletContextRequest; + private final HttpFields _httpFields; + private final HttpURI _uri; + + ServletCoreRequest(HttpServletRequest request) + { + _servletRequest = request; + _servletContextRequest = ServletContextRequest.getServletContextRequest(_servletRequest); + + HttpFields.Mutable fields = HttpFields.build(); + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) + { + String headerName = headerNames.nextElement(); + Enumeration headerValues = request.getHeaders(headerName); + while (headerValues.hasMoreElements()) + { + String headerValue = headerValues.nextElement(); + fields.add(new HttpField(headerName, headerValue)); + } + } + + _httpFields = fields.asImmutable(); + String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + boolean included = includedServletPath != null; + + HttpURI.Mutable builder = HttpURI.build(request.getRequestURI()); + if (included) + builder.path(addEncodedPaths(request.getContextPath(), encodePath(DefaultServlet.getIncludedPathInContext(request, includedServletPath, false)))); + else if (request.getDispatcherType() != DispatcherType.REQUEST) + builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo())))); + builder.query(request.getQueryString()); + _uri = builder.asImmutable(); + } + + @Override + public HttpFields getHeaders() + { + return _httpFields; + } + + @Override + public HttpURI getHttpURI() + { + return _uri; + } + + @Override + public String getId() + { + return _servletRequest.getRequestId(); + } + + @Override + public String getMethod() + { + return _servletRequest.getMethod(); + } + + public HttpServletRequest getServletRequest() + { + return _servletRequest; + } + + @Override + public boolean isSecure() + { + return _servletRequest.isSecure(); + } + + @Override + public Object removeAttribute(String name) + { + Object value = _servletRequest.getAttribute(name); + _servletRequest.removeAttribute(name); + return value; + } + + @Override + public Object setAttribute(String name, Object attribute) + { + Object value = _servletRequest.getAttribute(name); + _servletRequest.setAttribute(name, attribute); + return value; + } + + @Override + public Object getAttribute(String name) + { + return _servletRequest.getAttribute(name); + } + + @Override + public Set getAttributeNameSet() + { + Set set = new HashSet<>(); + Enumeration e = _servletRequest.getAttributeNames(); + while (e.hasMoreElements()) + { + set.add(e.nextElement()); + } + return set; + } + + @Override + public void clearAttributes() + { + Enumeration e = _servletRequest.getAttributeNames(); + while (e.hasMoreElements()) + { + _servletRequest.removeAttribute(e.nextElement()); + } + } + + @Override + public void fail(Throwable failure) + { + throw new UnsupportedOperationException(); + } + + @Override + public Components getComponents() + { + return _servletContextRequest.getComponents(); + } + + @Override + public ConnectionMetaData getConnectionMetaData() + { + return _servletContextRequest.getConnectionMetaData(); + } + + @Override + public Context getContext() + { + return _servletContextRequest.getContext(); + } + + @Override + public void demand(Runnable demandCallback) + { + throw new UnsupportedOperationException(); + } + + @Override + public HttpFields getTrailers() + { + return _servletContextRequest.getTrailers(); + } + + @Override + public long getBeginNanoTime() + { + return _servletContextRequest.getBeginNanoTime(); + } + + @Override + public long getHeadersNanoTime() + { + return _servletContextRequest.getHeadersNanoTime(); + } + + @Override + public Content.Chunk read() + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean consumeAvailable() + { + throw new UnsupportedOperationException(); + } + + @Override + public void addIdleTimeoutListener(Predicate onIdleTimeout) + { + _servletContextRequest.addIdleTimeoutListener(onIdleTimeout); + } + + @Override + public void addFailureListener(Consumer onFailure) + { + _servletContextRequest.addFailureListener(onFailure); + } + + @Override + public TunnelSupport getTunnelSupport() + { + return null; + } + + @Override + public void addHttpStreamWrapper(Function wrapper) + { + _servletContextRequest.addHttpStreamWrapper(wrapper); + } + + @Override + public Session getSession(boolean create) + { + return Session.getSession(_servletRequest.getSession(create)); + } +} diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreResponse.java new file mode 100644 index 00000000000..a11ae77b6bb --- /dev/null +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreResponse.java @@ -0,0 +1,379 @@ +// +// ======================================================================== +// 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.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.util.EnumSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import jakarta.servlet.http.HttpServletResponse; +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.io.ByteBufferInputStream; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; + +/** + * A {@link HttpServletResponse} wrapped as a core {@link Response}. + * All write operations are internally converted to blocking writes on the servlet API. + */ +class ServletCoreResponse implements Response +{ + private final HttpServletResponse _response; + private final Request _coreRequest; + private final HttpFields.Mutable _httpFields; + private final boolean _included; + private final ServletContextResponse _servletContextResponse; + + public ServletCoreResponse(Request coreRequest, HttpServletResponse response, boolean included) + { + _coreRequest = coreRequest; + _response = response; + _servletContextResponse = ServletContextResponse.getServletContextResponse(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 + public HttpFields.Mutable getHeaders() + { + return _httpFields; + } + + public HttpServletResponse getServletResponse() + { + return _response; + } + + @Override + public boolean hasLastWrite() + { + return _servletContextResponse.hasLastWrite(); + } + + @Override + public boolean isCompletedSuccessfully() + { + return _servletContextResponse.isCompletedSuccessfully(); + } + + @Override + public boolean isCommitted() + { + return _response.isCommitted(); + } + + private boolean isWriting() + { + return _servletContextResponse.isWriting(); + } + + @Override + public void write(boolean last, ByteBuffer byteBuffer, Callback callback) + { + if (_included) + last = false; + try + { + if (BufferUtil.hasContent(byteBuffer)) + { + if (isWriting()) + { + String characterEncoding = _response.getCharacterEncoding(); + try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer); + InputStreamReader reader = new InputStreamReader(bbis, characterEncoding)) + { + IO.copy(reader, _response.getWriter()); + } + + if (last) + _response.getWriter().close(); + } + else + { + BufferUtil.writeTo(byteBuffer, _response.getOutputStream()); + if (last) + _response.getOutputStream().close(); + } + } + + callback.succeeded(); + } + catch (Throwable t) + { + callback.failed(t); + } + } + + @Override + public Request getRequest() + { + return _coreRequest; + } + + @Override + public int getStatus() + { + return _response.getStatus(); + } + + @Override + public void setStatus(int code) + { + if (_included) + return; + _response.setStatus(code); + } + + @Override + public Supplier getTrailersSupplier() + { + return null; + } + + @Override + public void setTrailersSupplier(Supplier trailers) + { + } + + @Override + public void reset() + { + _response.reset(); + } + + @Override + public CompletableFuture writeInterim(int status, HttpFields headers) + { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _response); + } + + private static class HttpServletResponseHttpFields implements HttpFields.Mutable + { + private final HttpServletResponse _response; + + private HttpServletResponseHttpFields(HttpServletResponse response) + { + _response = response; + } + + @Override + public ListIterator listIterator() + { + // The minimum requirement is to implement the listIterator, but it is inefficient. + // Other methods are implemented for efficiency. + final ListIterator list = _response.getHeaderNames().stream() + .map(n -> new HttpField(n, _response.getHeader(n))) + .collect(Collectors.toList()) + .listIterator(); + + return new ListIterator<>() + { + HttpField _last; + + @Override + public boolean hasNext() + { + return list.hasNext(); + } + + @Override + public HttpField next() + { + return _last = list.next(); + } + + @Override + public boolean hasPrevious() + { + return list.hasPrevious(); + } + + @Override + public HttpField previous() + { + return _last = list.previous(); + } + + @Override + public int nextIndex() + { + return list.nextIndex(); + } + + @Override + public int previousIndex() + { + return list.previousIndex(); + } + + @Override + public void remove() + { + if (_last != null) + { + // This is not exactly the right semantic for repeated field names + list.remove(); + _response.setHeader(_last.getName(), null); + } + } + + @Override + public void set(HttpField httpField) + { + list.set(httpField); + _response.setHeader(httpField.getName(), httpField.getValue()); + } + + @Override + public void add(HttpField httpField) + { + list.add(httpField); + _response.addHeader(httpField.getName(), httpField.getValue()); + } + }; + } + + @Override + public Mutable add(String name, String value) + { + _response.addHeader(name, value); + return this; + } + + @Override + public Mutable add(HttpHeader header, HttpHeaderValue value) + { + _response.addHeader(header.asString(), value.asString()); + return this; + } + + @Override + public Mutable add(HttpHeader header, String value) + { + _response.addHeader(header.asString(), value); + return this; + } + + @Override + public Mutable add(HttpField field) + { + _response.addHeader(field.getName(), field.getValue()); + return this; + } + + @Override + public Mutable put(HttpField field) + { + _response.setHeader(field.getName(), field.getValue()); + return this; + } + + @Override + public Mutable put(String name, String value) + { + _response.setHeader(name, value); + return this; + } + + @Override + public Mutable put(HttpHeader header, HttpHeaderValue value) + { + _response.setHeader(header.asString(), value.asString()); + return this; + } + + @Override + public Mutable put(HttpHeader header, String value) + { + _response.setHeader(header.asString(), value); + return this; + } + + @Override + public Mutable put(String name, List list) + { + Objects.requireNonNull(name); + Objects.requireNonNull(list); + boolean first = true; + for (String s : list) + { + if (first) + _response.setHeader(name, s); + else + _response.addHeader(name, s); + first = false; + } + return this; + } + + @Override + public Mutable remove(HttpHeader header) + { + _response.setHeader(header.asString(), null); + return this; + } + + @Override + public Mutable remove(EnumSet fields) + { + for (HttpHeader header : fields) + remove(header); + return this; + } + + @Override + public Mutable remove(String name) + { + _response.setHeader(name, null); + return 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 05c2b6476a9..38c8586dbd4 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 @@ -2035,7 +2035,7 @@ public class DefaultServletTest String body = response.getContent(); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); - assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); + // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length()))); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); String boundary = getContentTypeBoundary(contentType); @@ -2063,7 +2063,7 @@ public class DefaultServletTest String body = response.getContent(); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); - assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); + // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length()))); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); String boundary = getContentTypeBoundary(contentType); @@ -2093,7 +2093,7 @@ public class DefaultServletTest String body = response.getContent(); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); - assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); + // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length()))); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); String boundary = getContentTypeBoundary(contentType); @@ -2154,7 +2154,7 @@ public class DefaultServletTest String body = response.getContent(); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); - assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); + // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length()))); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); String boundary = getContentTypeBoundary(contentType); @@ -2183,7 +2183,7 @@ public class DefaultServletTest String body = response.getContent(); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); - assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); + // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length()))); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); String boundary = getContentTypeBoundary(contentType); @@ -2295,7 +2295,7 @@ public class DefaultServletTest HttpTester.Response response = HttpTester.parseResponse(rawResponse); assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200)); String body = response.getContent(); - assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "" + body.length())); + assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, String.valueOf(body.length()))); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_TYPE, "text/plain;charset=UTF-8")); assertThat(body, containsString("Extra Info")); @@ -2308,7 +2308,7 @@ public class DefaultServletTest response = HttpTester.parseResponse(rawResponse); assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200)); body = response.getContent(); - assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "" + body.length())); + assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, String.valueOf(body.length()))); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_TYPE, "image/jpeg;charset=utf-8")); assertThat(body, containsString("Extra Info")); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java index e3f1042f53d..133845af7b2 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java @@ -16,6 +16,8 @@ package org.eclipse.jetty.ee10.servlet; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; @@ -23,6 +25,7 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -57,6 +60,7 @@ import org.eclipse.jetty.session.SessionDataStoreFactory; import org.eclipse.jetty.toolchain.test.IO; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.URIUtil; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -359,6 +363,162 @@ public class SessionHandlerTest } } + @Test + public void testRequestedSessionIdFromCookie() throws Exception + { + String contextPath = "/"; + String servletMapping = "/server"; + + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + DefaultSessionIdManager sessionIdManager = new DefaultSessionIdManager(server); + server.addBean(sessionIdManager, true); + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); + server.addBean(cacheFactory); + + SessionDataStoreFactory storeFactory = new NullSessionDataStoreFactory(); + server.addBean(storeFactory); + + HouseKeeper housekeeper = new HouseKeeper(); + housekeeper.setIntervalSec(-1); //turn off scavenging + sessionIdManager.setSessionHouseKeeper(housekeeper); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath(contextPath); + server.setHandler(context); + SessionHandler sessionHandler = new SessionHandler(); + sessionHandler.setSessionIdManager(sessionIdManager); + sessionHandler.setMaxInactiveInterval(-1); //immortal session + context.setSessionHandler(sessionHandler); + + TestRequestedSessionIdServlet servlet = new TestRequestedSessionIdServlet(); + ServletHolder holder = new ServletHolder(servlet); + context.addServlet(holder, servletMapping); + + server.start(); + int port = connector.getLocalPort(); + try (StacklessLogging stackless = new StacklessLogging(SessionHandlerTest.class.getPackage())) + { + HttpClient client = new HttpClient(); + client.start(); + + //test with no session cookie + String path = contextPath + (contextPath.endsWith("/") && servletMapping.startsWith("/") ? servletMapping.substring(1) : servletMapping); + String url = "http://localhost:" + port + path; + ContentResponse response = client.GET(url); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertThat(response.getContentAsString(), containsString("valid=false")); + + //test with a cookie for non-existant session + URI uri = URIUtil.toURI(URIUtil.newURI("http", "localhost", port, path, "")); + HttpCookie cookie = HttpCookie.build(SessionHandler.__DefaultSessionCookie, "123456789").path("/").domain("localhost").build(); + client.getHttpCookieStore().add(uri, cookie); + response = client.GET(url); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + String content = response.getContentAsString(); + assertThat(content, containsString("requestedId=123456789")); + assertThat(content, containsString("valid=false")); + + //Get rid of fake cookie + client.getHttpCookieStore().clear(); + + //Make a real session + response = client.GET(url + "?action=create"); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertNotNull(response.getHeaders().get("Set-Cookie")); + + //Check the requestedSessionId is valid + response = client.GET(url); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + content = response.getContentAsString(); + assertThat(content, containsString("valid=true")); + } + finally + { + server.stop(); + } + } + + @Test + public void testRequestedSessionIdFromURL() throws Exception + { + String contextPath = "/"; + String servletMapping = "/server"; + + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + DefaultSessionIdManager sessionIdManager = new DefaultSessionIdManager(server); + server.addBean(sessionIdManager, true); + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); + server.addBean(cacheFactory); + + SessionDataStoreFactory storeFactory = new NullSessionDataStoreFactory(); + server.addBean(storeFactory); + + HouseKeeper housekeeper = new HouseKeeper(); + housekeeper.setIntervalSec(-1); //turn off scavenging + sessionIdManager.setSessionHouseKeeper(housekeeper); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath(contextPath); + server.setHandler(context); + SessionHandler sessionHandler = new SessionHandler(); + sessionHandler.setUsingCookies(false); + sessionHandler.setSessionIdManager(sessionIdManager); + sessionHandler.setMaxInactiveInterval(-1); //immortal session + context.setSessionHandler(sessionHandler); + + TestRequestedSessionIdServlet servlet = new TestRequestedSessionIdServlet(); + ServletHolder holder = new ServletHolder(servlet); + context.addServlet(holder, servletMapping); + + server.start(); + int port = connector.getLocalPort(); + try (StacklessLogging stackless = new StacklessLogging(SessionHandlerTest.class.getPackage())) + { + HttpClient client = new HttpClient(); + client.start(); + + //test with no session cookie + String path = contextPath + (contextPath.endsWith("/") && servletMapping.startsWith("/") ? servletMapping.substring(1) : servletMapping); + String url = "http://localhost:" + port + path; + ContentResponse response = client.GET(url); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertThat(response.getContentAsString(), containsString("valid=false")); + + //test with id for non-existent session + response = client.GET(url + ";" + SessionHandler.__DefaultSessionIdPathParameterName + "=" + "123456789"); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + String content = response.getContentAsString(); + assertThat(content, containsString("requestedId=123456789")); + assertThat(content, containsString("valid=false")); + + //Make a real session + response = client.GET(url + "?action=create"); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + content = response.getContentAsString(); + assertThat(content, containsString("createdId=")); + String sessionId = content.substring(content.indexOf("createdId=") + 10); + sessionId = sessionId.trim(); + + //Check the requestedSessionId is valid + response = client.GET(url + ";" + SessionHandler.__DefaultSessionIdPathParameterName + "=" + sessionId); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + content = response.getContentAsString(); + assertThat(content, containsString("valid=true")); + } + finally + { + server.stop(); + } + } + public static class TestServlet extends HttpServlet { private static final long serialVersionUID = 1L; @@ -386,6 +546,26 @@ public class SessionHandlerTest } } + public static class TestRequestedSessionIdServlet extends HttpServlet + { + private static final long serialVersionUID = 1L; + public String _id = null; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + PrintWriter writer = response.getWriter(); + writer.println("requestedId=" + request.getRequestedSessionId()); + writer.println("valid=" + request.isRequestedSessionIdValid()); + + if ("create".equals(request.getParameter("action"))) + { + HttpSession session = request.getSession(true); + writer.println("createdId=" + session.getId()); + } + } + } + public class MockSessionCache extends AbstractSessionCache { @@ -395,8 +575,9 @@ public class SessionHandlerTest } @Override - public void shutdown() + public ManagedSession doDelete(String key) { + return null; } @Override @@ -411,12 +592,6 @@ public class SessionHandlerTest return null; } - @Override - public ManagedSession doDelete(String key) - { - return null; - } - @Override public boolean doReplace(String id, ManagedSession oldValue, ManagedSession newValue) { @@ -429,6 +604,11 @@ public class SessionHandlerTest return null; } + @Override + public void shutdown() + { + } + @Override protected ManagedSession doComputeIfAbsent(String id, Function mappingFunction) { diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/rfcs/RFC2616BaseTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/rfcs/RFC2616BaseTest.java index 710ab5bd80b..b05bd09d089 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/rfcs/RFC2616BaseTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/rfcs/RFC2616BaseTest.java @@ -1117,12 +1117,12 @@ public abstract class RFC2616BaseTest HttpTester.Response response = responses.get(0); String specId = "10.3 Redirection HTTP/1.1 - basic (response 1)"; assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); - assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); + assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId); response = responses.get(1); specId = "10.3 Redirection HTTP/1.1 - basic (response 2)"; assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); - assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); + assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId); assertEquals("close", response.get("Connection"), specId); } @@ -1146,7 +1146,7 @@ public abstract class RFC2616BaseTest String specId = "10.3 Redirection HTTP/1.0 w/content"; assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); - assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R1.txt", response.get("Location"), specId); + assertEquals(getServer().getScheme() + "://localhost/tests/R1.txt", response.get("Location"), specId); } /** @@ -1169,7 +1169,7 @@ public abstract class RFC2616BaseTest String specId = "10.3 Redirection HTTP/1.1 w/content"; assertThat(specId + " [status]", response.getStatus(), is(HttpStatus.FOUND_302)); - assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R2.txt")); + assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost/tests/R2.txt")); assertThat(specId + " [connection]", response.get("Connection"), is("close")); } diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java index 51a14b2fffa..436127ed58c 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java @@ -447,7 +447,7 @@ public class MetaInfConfiguration extends AbstractConfiguration * Scan for META-INF/web-fragment.xml file in the given jar. * * @param context the context for the scan - * @param jar the jar resource to scan for fragements in + * @param jar the jar resource to scan for fragments in * @param cache the resource cache */ public void scanForFragment(WebAppContext context, Resource jar, ConcurrentHashMap cache) diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java index 7a05e100a82..d86c797282f 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java @@ -13,153 +13,578 @@ package org.eclipse.jetty.ee10.webapp; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.FileSystemPool; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceCollators; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; 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.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; +@ExtendWith(WorkDirExtension.class) public class MetaInfConfigurationTest { - public class TestableMetaInfConfiguration extends MetaInfConfiguration + @BeforeEach + public void beforeEach() { - List _expectedContainerScanTypes; - List _expectedWebAppScanTypes; - int _invocationCount = 0; - - public TestableMetaInfConfiguration(List expectedContainerScanTypes, List expectedWebAppScanTypes) - { - _expectedContainerScanTypes = expectedContainerScanTypes; - _expectedWebAppScanTypes = expectedWebAppScanTypes; - } - - @Override - public void scanJars(WebAppContext context, Collection jars, boolean useCaches, List scanTypes) throws Exception - { - assertNotNull(scanTypes); - List expectedScanTypes = null; - switch (_invocationCount) - { - case 0: - { - expectedScanTypes = _expectedContainerScanTypes; - break; - } - case 1: - { - expectedScanTypes = _expectedWebAppScanTypes; - break; - } - default: - { - fail("Too many invocations"); - } - } - - ++_invocationCount; - - assertNotNull(expectedScanTypes); - assertTrue(expectedScanTypes.containsAll(scanTypes)); - assertEquals(expectedScanTypes.size(), scanTypes.size()); - } + assertThat(FileSystemPool.INSTANCE.mounts(), empty()); } - @Test - public void testScanTypes() throws Exception + @AfterEach + public void tearDown() { - Path web25 = MavenTestingUtils.getTestResourcePathFile("web25.xml"); - Path web31 = MavenTestingUtils.getTestResourcePathFile("web31.xml"); - Path web31false = MavenTestingUtils.getTestResourcePathFile("web31false.xml"); - - //test a 2.5 webapp will not look for fragments as manually configured - MetaInfConfiguration meta25 = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes, - Arrays.asList(MetaInfConfiguration.METAINF_TLDS, MetaInfConfiguration.METAINF_RESOURCES)); - WebAppContext context25 = new WebAppContext(); - context25.setConfigurationDiscovered(false); - context25.getMetaData().setWebDescriptor(new WebDescriptor(context25.getResourceFactory().newResource(web25))); - context25.getContext().getServletContext().setEffectiveMajorVersion(2); - context25.getContext().getServletContext().setEffectiveMinorVersion(5); - meta25.preConfigure(context25); - - //test a 2.5 webapp will look for fragments as configurationDiscovered default true - MetaInfConfiguration meta25b = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes, - MetaInfConfiguration.__allScanTypes); - WebAppContext context25b = new WebAppContext(); - context25b.getMetaData().setWebDescriptor(new WebDescriptor(context25b.getResourceFactory().newResource(web25))); - context25b.getContext().getServletContext().setEffectiveMajorVersion(2); - context25b.getContext().getServletContext().setEffectiveMinorVersion(5); - meta25b.preConfigure(context25b); - - //test a 3.x metadata-complete webapp will not look for fragments - MetaInfConfiguration meta31 = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes, - Arrays.asList(MetaInfConfiguration.METAINF_TLDS, MetaInfConfiguration.METAINF_RESOURCES)); - WebAppContext context31 = new WebAppContext(); - context31.getMetaData().setWebDescriptor(new WebDescriptor(context31.getResourceFactory().newResource(web31))); - context31.getContext().getServletContext().setEffectiveMajorVersion(3); - context31.getContext().getServletContext().setEffectiveMinorVersion(1); - meta31.preConfigure(context31); - - //test a 3.x non metadata-complete webapp will look for fragments - MetaInfConfiguration meta31false = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes, - MetaInfConfiguration.__allScanTypes); - WebAppContext context31false = new WebAppContext(); - context31false.setConfigurationDiscovered(true); - context31false.getMetaData().setWebDescriptor(new WebDescriptor(context31false.getResourceFactory().newResource(web31false))); - context31false.getContext().getServletContext().setEffectiveMajorVersion(3); - context31false.getContext().getServletContext().setEffectiveMinorVersion(1); - meta31false.preConfigure(context31false); + assertThat(FileSystemPool.INSTANCE.mounts(), empty()); } /** - * This test examines both the classpath and the module path to find - * container resources. - * NOTE: the behaviour of the surefire plugin 3.0.0.M2 is different in - * jetty-9.4.x to jetty-10.0.x (where we use module-info): in jetty-9.4.x, - * we can use the --add-module argument to put the foo-bar-janb.jar onto the - * module path, but this doesn't seem to work in jetty-10.0.x. So this test - * will find foo-bar.janb.jar on the classpath, and jetty-util from the module path. - * - * @throws Exception if the test fails + * Test of a MetaInf scan of a Servlet 2.5 webapp, where + * {@link WebAppContext#setConfigurationDiscovered(boolean)} set to {@code false}, + * thus not performing any Servlet 3.0+ discovery steps for {@code META-INF/web-fragment.xml}. + * Scanning for {@code META-INF/resources} is unaffected by configuration. */ @Test - public void testFindAndFilterContainerPathsJDK9() throws Exception + public void testScanServlet25ConfigurationDiscoveredOff(WorkDir workDir) throws Exception + { + Path webappDir = workDir.getEmptyPathDir(); + Path webinf = webappDir.resolve("WEB-INF"); + FS.ensureDirExists(webinf); + Path webxml = webinf.resolve("web.xml"); + + String web25 = """ + + + Test 2.5 WebApp + + """; + + Files.writeString(webxml, web25, StandardCharsets.UTF_8); + Path libDir = webinf.resolve("lib"); + FS.ensureDirExists(libDir); + Path fooFragmentJar = libDir.resolve("foo-fragment.jar"); + try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar)) + { + Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml"); + FS.ensureDirExists(webfragment.getParent()); + Files.writeString(webfragment, "", StandardCharsets.UTF_8); + } + + Path barResourceJar = libDir.resolve("bar-resources.jar"); + try (FileSystem jarfs = createNewJarFileSystem(barResourceJar)) + { + Path resourcesDir = jarfs.getPath("/META-INF/resources"); + Files.createDirectories(resourcesDir); + Path testTxt = resourcesDir.resolve("test.txt"); + Files.writeString(testTxt, "Test", StandardCharsets.UTF_8); + } + + Path zedTldJar = libDir.resolve("zed-tlds.jar"); + try (FileSystem jarfs = createNewJarFileSystem(zedTldJar)) + { + Path tldFile = jarfs.getPath("/META-INF/zed.tld"); + Files.createDirectory(tldFile.getParent()); + Files.writeString(tldFile, "", StandardCharsets.UTF_8); + } + + WebAppContext context = new WebAppContext(); + context.setServer(new Server()); + try + { + context.setBaseResource(context.getResourceFactory().newResource(webappDir)); + context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml))); + context.setConfigurationDiscovered(false); // don't allow discovery of servlet 3.0+ features + context.getContext().getServletContext().setEffectiveMajorVersion(2); + context.getContext().getServletContext().setEffectiveMinorVersion(5); + + MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration(); + metaInfConfiguration.preConfigure(context); + + List discoveredWebInfResources = context.getMetaData().getWebInfResources(false) + .stream() + .sorted(ResourceCollators.byName(true)) + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedWebInfResources = { + fooFragmentJar.toUri().toASCIIString(), + barResourceJar.toUri().toASCIIString(), + zedTldJar.toUri().toASCIIString() + }; + assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources)); + + // Since this is Servlet 2.5, and we have configuration-discovered turned off, we shouldn't see any web fragments + Map fragmentMap = getDiscoveredMetaInfFragments(context); + assertThat("META-INF/web-fragment.xml discovered (servlet 2.5 and configuration-discovered turned off)", fragmentMap.size(), is(0)); + + // Even on Servlet 2.5, when we have configuration-discovered turned off, we should still see the META-INF/resources/ + Set resourceSet = getDiscoveredMetaInfResource(context); + assertThat(resourceSet.size(), is(1)); + List discoveredResources = resourceSet + .stream() + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedResources = { + URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/" + }; + assertThat("META-INF/resources discovered (servlet 2.5 and configuration-discovered turned off)", discoveredResources, hasItems(expectedResources)); + + // TLDs discovered + Set tldSet = getDiscoveredMetaInfTlds(context); + assertThat(tldSet.size(), is(1)); + List discoveredTlds = tldSet + .stream() + .map(URL::toExternalForm) + .toList(); + String[] expectedTlds = { + URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld" + }; + assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds)); + } + finally + { + LifeCycle.stop(context.getResourceFactory()); + } + } + + /** + * Test of a MetaInf scan of a Servlet 2.5 webapp, where + * {@link WebAppContext#setConfigurationDiscovered(boolean)} is left at default (@{code true}) + * allowing the performing of Servlet 3.0+ discovery steps for {@code META-INF/web-fragment.xml} and {@code META-INF/resources} + */ + @Test + public void testScanServlet25ConfigurationDiscoveredDefault(WorkDir workDir) throws Exception + { + Path webappDir = workDir.getEmptyPathDir(); + Path webinf = webappDir.resolve("WEB-INF"); + FS.ensureDirExists(webinf); + Path webxml = webinf.resolve("web.xml"); + + String web25 = """ + + + Test 2.5 WebApp + + """; + + Files.writeString(webxml, web25, StandardCharsets.UTF_8); + Path libDir = webinf.resolve("lib"); + FS.ensureDirExists(libDir); + Path fooFragmentJar = libDir.resolve("foo-fragment.jar"); + try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar)) + { + Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml"); + FS.ensureDirExists(webfragment.getParent()); + Files.writeString(webfragment, "", StandardCharsets.UTF_8); + } + + Path barResourceJar = libDir.resolve("bar-resources.jar"); + try (FileSystem jarfs = createNewJarFileSystem(barResourceJar)) + { + Path resourcesDir = jarfs.getPath("/META-INF/resources"); + Files.createDirectories(resourcesDir); + Path testTxt = resourcesDir.resolve("test.txt"); + Files.writeString(testTxt, "Test", StandardCharsets.UTF_8); + } + + Path zedTldJar = libDir.resolve("zed-tlds.jar"); + try (FileSystem jarfs = createNewJarFileSystem(zedTldJar)) + { + Path tldFile = jarfs.getPath("/META-INF/zed.tld"); + Files.createDirectory(tldFile.getParent()); + Files.writeString(tldFile, "", StandardCharsets.UTF_8); + } + + WebAppContext context = new WebAppContext(); + context.setServer(new Server()); + try + { + context.setBaseResource(context.getResourceFactory().newResource(webappDir)); + context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml))); + // context25.setConfigurationDiscovered(true); // The default value + context.getContext().getServletContext().setEffectiveMajorVersion(2); + context.getContext().getServletContext().setEffectiveMinorVersion(5); + + MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration(); + metaInfConfiguration.preConfigure(context); + + List discoveredWebInfResources = context.getMetaData().getWebInfResources(false) + .stream() + .sorted(ResourceCollators.byName(true)) + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedWebInfResources = { + fooFragmentJar.toUri().toASCIIString(), + barResourceJar.toUri().toASCIIString(), + zedTldJar.toUri().toASCIIString() + }; + assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources)); + + // Since this is Servlet 2.5, and we have configuration-discovered turned on, we should see the META-INF/web-fragment.xml entries + Map fragmentMap = getDiscoveredMetaInfFragments(context); + assertThat(fragmentMap.size(), is(1)); + List discoveredFragments = fragmentMap.entrySet() + .stream() + .map(e -> e.getValue().getURI().toASCIIString()) + .toList(); + String[] expectedFragments = { + URIUtil.toJarFileUri(fooFragmentJar.toUri()).toASCIIString() + "META-INF/web-fragment.xml" + }; + assertThat("META-INF/web-fragment.xml discovered (servlet 2.5 and configuration-discovered=true)", discoveredFragments, hasItems(expectedFragments)); + + // Since this is Servlet 2.5, and we have configuration-discovered turned on, we should see the META-INF/resources/ + Set resourceSet = getDiscoveredMetaInfResource(context); + assertThat(resourceSet.size(), is(1)); + List discoveredResources = resourceSet + .stream() + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedResources = { + URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/" + }; + assertThat("META-INF/resources discovered (servlet 2.5 and configuration-discovered=true)", discoveredResources, hasItems(expectedResources)); + + // TLDs discovered + Set tldSet = getDiscoveredMetaInfTlds(context); + assertThat(tldSet.size(), is(1)); + List discoveredTlds = tldSet + .stream() + .map(URL::toExternalForm) + .toList(); + String[] expectedTlds = { + URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld" + }; + assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds)); + } + finally + { + LifeCycle.stop(context.getResourceFactory()); + } + } + + /** + * Test of a MetaInf scan of a Servlet 3.0 webapp, metadata-complete is set to {@code false} + */ + @Test + public void testScanServlet30MetadataCompleteFalse(WorkDir workDir) throws Exception + { + Path webappDir = workDir.getEmptyPathDir(); + Path webinf = webappDir.resolve("WEB-INF"); + FS.ensureDirExists(webinf); + Path webxml = webinf.resolve("web.xml"); + + String web30 = """ + + + Test 3.0 WebApp + + """; + + Files.writeString(webxml, web30, StandardCharsets.UTF_8); + Path libDir = webinf.resolve("lib"); + FS.ensureDirExists(libDir); + Path fooFragmentJar = libDir.resolve("foo-fragment.jar"); + try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar)) + { + Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml"); + FS.ensureDirExists(webfragment.getParent()); + Files.writeString(webfragment, "", StandardCharsets.UTF_8); + } + + Path barResourceJar = libDir.resolve("bar-resources.jar"); + try (FileSystem jarfs = createNewJarFileSystem(barResourceJar)) + { + Path resourcesDir = jarfs.getPath("/META-INF/resources"); + Files.createDirectories(resourcesDir); + Path testTxt = resourcesDir.resolve("test.txt"); + Files.writeString(testTxt, "Test", StandardCharsets.UTF_8); + } + + Path zedTldJar = libDir.resolve("zed-tlds.jar"); + try (FileSystem jarfs = createNewJarFileSystem(zedTldJar)) + { + Path tldFile = jarfs.getPath("/META-INF/zed.tld"); + Files.createDirectory(tldFile.getParent()); + Files.writeString(tldFile, "", StandardCharsets.UTF_8); + } + + WebAppContext context = new WebAppContext(); + context.setServer(new Server()); + try + { + context.setBaseResource(context.getResourceFactory().newResource(webappDir)); + context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml))); + // context25.setConfigurationDiscovered(true); // The default value + context.getContext().getServletContext().setEffectiveMajorVersion(3); + context.getContext().getServletContext().setEffectiveMinorVersion(0); + + MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration(); + metaInfConfiguration.preConfigure(context); + + List discoveredWebInfResources = context.getMetaData().getWebInfResources(false) + .stream() + .sorted(ResourceCollators.byName(true)) + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedWebInfResources = { + fooFragmentJar.toUri().toASCIIString(), + barResourceJar.toUri().toASCIIString(), + zedTldJar.toUri().toASCIIString() + }; + assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources)); + + // Since this is Servlet 3.0, and we have configuration-discovered turned on, we should see the META-INF/web-fragment.xml entries + Map fragmentMap = getDiscoveredMetaInfFragments(context); + assertThat(fragmentMap.size(), is(1)); + List discoveredFragments = fragmentMap.entrySet() + .stream() + .map(e -> e.getValue().getURI().toASCIIString()) + .toList(); + String[] expectedFragments = { + URIUtil.toJarFileUri(fooFragmentJar.toUri()).toASCIIString() + "META-INF/web-fragment.xml" + }; + assertThat("META-INF/web-fragment.xml discovered (servlet 3.0, and metadata-complete=false, and configuration-discovered=true)", discoveredFragments, hasItems(expectedFragments)); + + // Since this is Servlet 3.0, and we have configuration-discovered turned on, we should see the META-INF/resources/ + Set resourceSet = getDiscoveredMetaInfResource(context); + assertThat(resourceSet.size(), is(1)); + List discoveredResources = resourceSet + .stream() + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedResources = { + URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/" + }; + assertThat("META-INF/resources discovered (servlet 3.0, and metadata-complete=false, and configuration-discovered=true)", discoveredResources, hasItems(expectedResources)); + + // TLDs discovered + Set tldSet = getDiscoveredMetaInfTlds(context); + assertThat(tldSet.size(), is(1)); + List discoveredTlds = tldSet + .stream() + .map(URL::toExternalForm) + .toList(); + String[] expectedTlds = { + URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld" + }; + assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds)); + } + finally + { + LifeCycle.stop(context.getResourceFactory()); + } + } + + /** + * Test of a MetaInf scan of a Servlet 3.1 webapp, metadata-complete is set to {@code true} + */ + @Test + public void testScanServlet31MetadataCompleteTrue(WorkDir workDir) throws Exception + { + Path webappDir = workDir.getEmptyPathDir(); + Path webinf = webappDir.resolve("WEB-INF"); + FS.ensureDirExists(webinf); + Path webxml = webinf.resolve("web.xml"); + + String web31 = """ + + + Test 3.1 WebApp + + """; + + Files.writeString(webxml, web31, StandardCharsets.UTF_8); + Path libDir = webinf.resolve("lib"); + FS.ensureDirExists(libDir); + Path fooFragmentJar = libDir.resolve("foo-fragment.jar"); + try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar)) + { + Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml"); + FS.ensureDirExists(webfragment.getParent()); + Files.writeString(webfragment, "", StandardCharsets.UTF_8); + } + + Path barResourceJar = libDir.resolve("bar-resources.jar"); + try (FileSystem jarfs = createNewJarFileSystem(barResourceJar)) + { + Path resourcesDir = jarfs.getPath("/META-INF/resources"); + Files.createDirectories(resourcesDir); + Path testTxt = resourcesDir.resolve("test.txt"); + Files.writeString(testTxt, "Test", StandardCharsets.UTF_8); + } + + Path zedTldJar = libDir.resolve("zed-tlds.jar"); + try (FileSystem jarfs = createNewJarFileSystem(zedTldJar)) + { + Path tldFile = jarfs.getPath("/META-INF/zed.tld"); + Files.createDirectory(tldFile.getParent()); + Files.writeString(tldFile, "", StandardCharsets.UTF_8); + } + + WebAppContext context = new WebAppContext(); + context.setServer(new Server()); + try + { + context.setBaseResource(context.getResourceFactory().newResource(webappDir)); + context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml))); + context.getContext().getServletContext().setEffectiveMajorVersion(3); + context.getContext().getServletContext().setEffectiveMinorVersion(1); + + MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration(); + metaInfConfiguration.preConfigure(context); + + List discoveredWebInfResources = context.getMetaData().getWebInfResources(false) + .stream() + .sorted(ResourceCollators.byName(true)) + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedWebInfResources = { + fooFragmentJar.toUri().toASCIIString(), + barResourceJar.toUri().toASCIIString(), + zedTldJar.toUri().toASCIIString() + }; + assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources)); + + // Since this is Servlet 3.1, and we have metadata-complete=true, we should see no fragments + Map fragmentMap = getDiscoveredMetaInfFragments(context); + assertThat("META-INF/web-fragment.xml discovered (servlet 3.1, and metadata-complete=true)", fragmentMap.size(), is(0)); + + // Even on Servlet 3.1, with metadata-complete=true, we should still see the META-INF/resources/ + Set resourceSet = getDiscoveredMetaInfResource(context); + assertThat(resourceSet.size(), is(1)); + List discoveredResources = resourceSet + .stream() + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + String[] expectedResources = { + URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/" + }; + assertThat("META-INF/resources discovered (servlet 3.1 and metadata-complete=true)", discoveredResources, hasItems(expectedResources)); + + // TLDs discovered + Set tldSet = getDiscoveredMetaInfTlds(context); + assertThat(tldSet.size(), is(1)); + List discoveredTlds = tldSet + .stream() + .map(URL::toExternalForm) + .toList(); + String[] expectedTlds = { + URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld" + }; + assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds)); + } + finally + { + LifeCycle.stop(context.getResourceFactory()); + } + } + + private FileSystem createNewJarFileSystem(Path jarFile) throws IOException + { + Map env = new HashMap<>(); + env.put("create", "true"); + URI jarUri = URIUtil.uriJarPrefix(jarFile.toUri(), "!/"); + return FileSystems.newFileSystem(jarUri, env); + } + + /** + * This test examines both the classpath and the module path to find container resources. + * This test looks {@code foo-bar.janb.jar} on the classpath (it was added there by the surefire configuration + * present in the {@code pom.xml}), and the {@code servlet-api} from the module path. + */ + @Test + public void testGetContainerPathsWithModuleSystem() throws Exception { MetaInfConfiguration config = new MetaInfConfiguration(); WebAppContext context = new WebAppContext(); context.setServer(new Server()); - config.preConfigure(context); try { context.setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN, ".*servlet-api-[^/]*\\.jar$|.*/foo-bar-janb.jar"); WebAppClassLoader loader = new WebAppClassLoader(context); context.setClassLoader(loader); - config.findAndFilterContainerPaths(context); - List containerResources = context.getMetaData().getContainerResources(); + config.preConfigure(context); - assertEquals(2, containerResources.size()); - for (Resource r : containerResources) - { - String s = r.toString(); - assertTrue(s.endsWith("foo-bar-janb.jar") || s.contains("servlet-api")); - } + Class janbClazz = Class.forName("foo.bar.janb.What", false, loader); + URI janbUri = TypeUtil.getLocationOfClass(janbClazz); + Class servletClazz = Class.forName("jakarta.servlet.Servlet", false, loader); + URI servletUri = TypeUtil.getLocationOfClass(servletClazz); + + List discoveredContainerResources = context.getMetaData().getContainerResources() + .stream() + .sorted(ResourceCollators.byName(true)) + .map(Resource::getURI) + .map(URI::toASCIIString) + .toList(); + // we "correct" the bad file URLs that come from the ClassLoader + // to be the same as what comes from every non-classloader URL/URI. + String[] expectedContainerResources = { + URIUtil.correctFileURI(janbUri).toASCIIString(), + URIUtil.correctFileURI(servletUri).toASCIIString() + }; + assertThat("Discovered Container resources", discoveredContainerResources, hasItems(expectedContainerResources)); } finally { config.postConfigure(context); + // manually stop ResourceFactory. + // normally this would be done via WebAppContext.stop(), but we didn't start the context. + LifeCycle.stop(context.getResourceFactory()); } } + + private Map getDiscoveredMetaInfFragments(WebAppContext context) + { + return (Map)context.getAttribute(MetaInfConfiguration.METAINF_FRAGMENTS); + } + + private Set getDiscoveredMetaInfResource(WebAppContext context) + { + return (Set)context.getAttribute(MetaInfConfiguration.METAINF_RESOURCES); + } + + private Set getDiscoveredMetaInfTlds(WebAppContext context) + { + return (Set)context.getAttribute(MetaInfConfiguration.METAINF_TLDS); + } } diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/resources/web25.xml b/jetty-ee10/jetty-ee10-webapp/src/test/resources/web25.xml deleted file mode 100644 index da2e65b6007..00000000000 --- a/jetty-ee10/jetty-ee10-webapp/src/test/resources/web25.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - Test 2.5 WebApp - - diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/resources/web31.xml b/jetty-ee10/jetty-ee10-webapp/src/test/resources/web31.xml deleted file mode 100644 index 768c5ea212f..00000000000 --- a/jetty-ee10/jetty-ee10-webapp/src/test/resources/web31.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Test 31 WebApp - - diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/resources/web31false.xml b/jetty-ee10/jetty-ee10-webapp/src/test/resources/web31false.xml deleted file mode 100644 index 81750df9dda..00000000000 --- a/jetty-ee10/jetty-ee10-webapp/src/test/resources/web31false.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Test 31 WebApp - - 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 5113c845ff2..62744880a71 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 @@ -1585,8 +1585,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable } catch (Throwable t) { - if (ExceptionUtil.areNotAssociated(e, t)) - e.addSuppressed(t); + ExceptionUtil.addSuppressedIfNotAssociated(e, t); } finally { 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 989dbf595a8..ff98449092c 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 @@ -2482,6 +2482,12 @@ public class ResponseTest return _committed; } + @Override + public boolean hasLastWrite() + { + return _last; + } + @Override public boolean isCompletedSuccessfully() { diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java index f839954c176..29bf24a2167 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java +++ b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java @@ -50,7 +50,6 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; -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.assertThrows; @@ -87,6 +86,7 @@ public class SessionHandlerTest String pathInContext = request.getPathInfo(); String[] split = pathInContext.substring(1).split("/"); + String requestedSessionId = request.getRequestedSessionId(); HttpSession session = request.getSession(false); if (split.length > 0) @@ -135,6 +135,10 @@ public class SessionHandlerTest } StringBuilder out = new StringBuilder(); + out.append("requestedSessionId=" + requestedSessionId).append('\n'); + out.append("requestedSessionIdValid=" + request.isRequestedSessionIdValid()).append('\n'); + + if (session == null) out.append("No Session\n"); else @@ -309,8 +313,9 @@ public class SessionHandlerTest String setCookie = response.get("SET-COOKIE"); assertThat(setCookie, containsString("Path=/")); String content = response.getContent(); - assertThat(content, startsWith("Session=")); - String id = content.substring(content.indexOf('=') + 1, content.indexOf('\n')); + assertThat(content, containsString("Session=")); + String id = content.substring(content.indexOf("Session=") + 8); + id = id.trim(); assertThat(id, not(equalTo("oldCookieId"))); endPoint.addInput(""" @@ -326,6 +331,64 @@ public class SessionHandlerTest assertThat(content, containsString("Session=" + id)); } + @Test + public void testRequestedSessionIdFromCookie() throws Exception + { + _server.stop(); + _sessionHandler.setSessionPath(null); + _contextHandler.setContextPath("/"); + _server.start(); + + //test with no session cookie + LocalConnector.LocalEndPoint endPoint = _connector.connect(); + endPoint.addInput(""" + GET / HTTP/1.1 + Host: localhost + + """); + + HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse()); + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getContent(), containsString("No Session")); + assertThat(response.getContent(), containsString("requestedSessionIdValid=false")); + + //test with a cookie for non-existant session + endPoint.addInput(""" + GET / HTTP/1.1 + Host: localhost + Cookie: JSESSIONID=%s + + """.formatted("123456789")); + response = HttpTester.parseResponse(endPoint.getResponse()); + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getContent(), containsString("No Session")); + assertThat(response.getContent(), containsString("requestedSessionIdValid=false")); + + //Make a real session + endPoint.addInput(""" + GET /create HTTP/1.1 + Host: localhost + + """); + + response = HttpTester.parseResponse(endPoint.getResponse()); + assertThat(response.getStatus(), equalTo(200)); + String content = response.getContent(); + assertThat(content, containsString("Session=")); + String id = content.substring(content.indexOf("Session=") + 8); + id = id.trim(); + + //Check the requestedSessionId is valid + endPoint.addInput(""" + GET / HTTP/1.1 + Host: localhost + Cookie: JSESSIONID=%s + + """.formatted(id)); + response = HttpTester.parseResponse(endPoint.getResponse()); + assertThat(response.getContent(), containsString("requestedSessionIdValid=true")); + } + @Test public void testSetAttribute() throws Exception { @@ -339,8 +402,9 @@ public class SessionHandlerTest HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse()); assertThat(response.getStatus(), equalTo(200)); String content = response.getContent(); - assertThat(content, startsWith("Session=")); - String id = content.substring(content.indexOf('=') + 1, content.indexOf('\n')); + assertThat(content, containsString("Session=")); + String id = content.substring(content.indexOf("Session=") + 8); + id = id.trim(); endPoint.addInput(""" GET /set/attribute/value HTTP/1.1 @@ -380,7 +444,7 @@ public class SessionHandlerTest HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse()); assertThat(response.getStatus(), equalTo(200)); String content = response.getContent(); - assertThat(content, startsWith("Session=")); + assertThat(content, containsString("Session=")); String setCookie = response.get(HttpHeader.SET_COOKIE); String id = setCookie.substring(setCookie.indexOf("JSESSIONID=") + 11, setCookie.indexOf("; Path=/")); diff --git a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/NoJspServlet.java b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/NoJspServlet.java index ea0e98e3b59..cde119d8e11 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/NoJspServlet.java +++ b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/NoJspServlet.java @@ -28,7 +28,7 @@ public class NoJspServlet extends HttpServlet protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException { if (!_warned) - getServletContext().log("No JSP support. Check that JSP jars are in lib/jsp and that the JSP option has been specified to start.jar"); + getServletContext().log("No JSP support. Check that the ee9-jsp module is enabled, or otherwise ensure the jsp jars are on the server classpath."); _warned = true; response.sendError(500, "JSP support not configured"); diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/rfcs/RFC2616BaseTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/rfcs/RFC2616BaseTest.java index 4f0bf90480d..25b633191c5 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/rfcs/RFC2616BaseTest.java +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/rfcs/RFC2616BaseTest.java @@ -1114,12 +1114,12 @@ public abstract class RFC2616BaseTest HttpTester.Response response = responses.get(0); String specId = "10.3 Redirection HTTP/1.1 - basic (response 1)"; assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); - assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); + assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId); response = responses.get(1); specId = "10.3 Redirection HTTP/1.1 - basic (response 2)"; assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); - assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); + assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId); assertEquals("close", response.get("Connection"), specId); } @@ -1143,7 +1143,7 @@ public abstract class RFC2616BaseTest String specId = "10.3 Redirection HTTP/1.0 w/content"; assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); - assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R1.txt", response.get("Location"), specId); + assertEquals(getServer().getScheme() + "://localhost/tests/R1.txt", response.get("Location"), specId); } /** @@ -1166,7 +1166,7 @@ public abstract class RFC2616BaseTest String specId = "10.3 Redirection HTTP/1.1 w/content"; assertThat(specId + " [status]", response.getStatus(), is(HttpStatus.FOUND_302)); - assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R2.txt")); + assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost/tests/R2.txt")); assertThat(specId + " [connection]", response.get("Connection"), is("close")); } diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/pom.xml b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/pom.xml index 073f5165a2b..92f955651d7 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/pom.xml +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/pom.xml @@ -37,7 +37,7 @@ org.eclipse.jetty.ee9 - jetty-ee9-annotations + jetty-ee9-webapp org.eclipse.jetty diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/pom.xml b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/pom.xml index 77dd62e3cf8..e8e5348e0df 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/pom.xml +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/pom.xml @@ -72,6 +72,11 @@ jetty-jmx test + + org.eclipse.jetty.ee9 + jetty-ee9-annotations + test + diff --git a/pom.xml b/pom.xml index a31e616a207..3525735c5c6 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ 2.20.0 1.56.1 2.10.1 - 32.1.1-jre + 32.1.2-jre 7.0.0 2.2 5.3.1 @@ -87,7 +87,7 @@ 2.2.1.Final 2.2.1.Final 3.5.3.Final - 2.3.0.Alpha1 + 3.0.1.Final 3.5.0.Final 1.1 1.0.7 @@ -95,7 +95,7 @@ 1.2 6.1 1.1 - 1.36 + 1.37 5.13.0 1.1.1 2.5.0 @@ -103,12 +103,12 @@ 10.0.14 2.0.3 2.20.0 - 1.4.8 + 1.4.9 3.1.4 10.3.6 3.9.0 0.13.1 - 1.9.14 + 1.9.15 3.9.0 3.12.11 0.9.1 @@ -151,7 +151,7 @@ 2.2.4 3.3.0 - 3.0.0 + 3.2.0 1.5.0 1.3.0 4.0.6 @@ -2222,7 +2222,7 @@ - 1.1.7 + 1.4.2 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 ea06afc67e5..71f1ca03304 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 @@ -50,6 +50,7 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.content.ByteBufferContentSource; import org.eclipse.jetty.tests.hometester.JettyHomeTester; +import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.PathMatchers; import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -1529,4 +1530,85 @@ public class DistributionTests extends AbstractJettyHomeTest } } } + + @ParameterizedTest + @ValueSource(strings = {"ee8", "ee9", "ee10"}) + public void testXmlDeployWarNotInWebapps(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("deploy", env) + "," + toEnvironment("webapp", 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, + "jetty.httpConfig.port=" + httpPort + }; + + // Put war into ${jetty.base}/wars/ directory + File srcWar = distribution.resolveArtifact("org.eclipse.jetty." + env + ".demos:jetty-" + env + "-demo-simple-webapp:war:" + jettyVersion); + Path warsDir = jettyBase.resolve("wars"); + FS.ensureDirExists(warsDir); + Path destWar = warsDir.resolve("demo.war"); + Files.copy(srcWar.toPath(), destWar); + + // Create XML for deployable + String xml = """ + + + + + /demo + %s + + """.formatted(env, destWar.toString()); + Files.writeString(jettyBase.resolve("webapps/demo.xml"), xml, StandardCharsets.UTF_8); + + // Specify Environment Properties for this raw XML based deployable + String props = """ + environment=%s + """.formatted(env); + Files.writeString(jettyBase.resolve("webapps/demo.properties"), props, StandardCharsets.UTF_8); + + /* The jetty.base tree should now look like this + * + * ${jetty.base} + * ├── resources/ + * │ └── jetty-logging.properties + * ├── start.d/ + * │ ├── ${env}-deploy.ini + * │ ├── ${env}-webapp.ini + * │ └── http.ini + * ├── wars/ + * │ └── demo.war + * ├── webapps/ + * │ ├── demo.properties + * │ └── demo.xml + * └── work/ + */ + + try (JettyHomeTester.Run runStart = distribution.start(argsStart)) + { + assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + + startHttpClient(); + ContentResponse response = client.GET("http://localhost:" + httpPort + "/demo/index.html"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } + } }