diff --git a/org.restlet.java/org.restlet.ext.jetty/README.md b/org.restlet.java/org.restlet.ext.jetty/README.md new file mode 100644 index 0000000000..a430b40dc6 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/README.md @@ -0,0 +1,32 @@ +# Debug Jetty + +## Add full logs + +[Jetty's documentation](https://jetty.org/docs/jetty/12/programming-guide/troubleshooting/logging.html) (the `org.eclipse.jetty:jetty-slf4j-impl`is already added to the `pom.xml`). + +Programmatically: +``` +System.setProperty("org.eclipse.jetty.LEVEL", "TRACE"); +``` + +Or add a `jetty-logging.properties`: +``` +org.eclipse.jetty.LEVEL=TRACE +org.eclipse.jetty.client.LEVEL=TRACE +``` + +## Debug using JMX +You need to update the current implementation by hand. + +- [activate JMX](https://jetty.org/docs/jetty/12/programming-guide/arch/jmx.html) +The Jetty server is created in class `JettyServerHelper`. + +``` + // Create an MBeanContainer with the platform MBeanServer. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + // Add MBeanContainer to the root component. + jettyServer.addBean(mbeanContainer); +``` + +- to [state tracking](https://jetty.org/docs/jetty/12/programming-guide/troubleshooting/state-tracking.html) + You can use [jconsole](https://docs.oracle.com/javase/8/docs/technotes/guides/management/jconsole.html) to check the state of MBean or just run operations on them. diff --git a/org.restlet.java/org.restlet.ext.jetty/pom.xml b/org.restlet.java/org.restlet.ext.jetty/pom.xml index 3d48e9ad02..cd996044f4 100644 --- a/org.restlet.java/org.restlet.ext.jetty/pom.xml +++ b/org.restlet.java/org.restlet.ext.jetty/pom.xml @@ -45,6 +45,11 @@ jetty-http3-client-transport ${lib-jetty-version} + + org.eclipse.jetty + jetty-slf4j-impl + ${lib-jetty-version} + org.restlet org.restlet diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java index 696591962e..6e534e77d5 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java @@ -1,22 +1,15 @@ /** * Copyright 2005-2024 Qlik - * + *

* The contents of this file is subject to the terms of the Apache 2.0 open * source license available at http://www.opensource.org/licenses/apache-2.0 - * + *

* Restlet is a registered trademark of QlikTech International AB. */ package org.restlet.ext.jetty; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.concurrent.Executor; -import java.util.logging.Level; - import org.eclipse.jetty.client.AuthenticationStore; -import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.HttpProxy; @@ -49,6 +42,12 @@ import org.restlet.ext.jetty.internal.JettyClientCall; import org.restlet.ext.jetty.internal.RestletSslContextFactoryClient; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.Executor; +import java.util.logging.Level; + /** * HTTP client connector using the Jetty project. Here is the list of parameters * that are supported. They should be set in the Client's context before it is @@ -201,22 +200,13 @@ * * For the default SSL parameters see the Javadocs of the * {@link DefaultSslContextFactory} class. - * + * * @author Jerome Louvel * @author Tal Liron */ public class HttpClientHelper extends org.restlet.engine.adapter.HttpClientHelper { - public static void main(String[] args) throws Exception { - Client client = new Client(Protocol.HTTP, Protocol.HTTPS); - HttpClientHelper helper = new HttpClientHelper(client); - helper.start(); - HttpClient httpClient = helper.getHttpClient(); - ContentResponse response = httpClient.GET("http://github.io/"); - response.getContentAsString(); - } - /** * The wrapped Jetty HTTP client. */ @@ -239,7 +229,7 @@ public static void main(String[] args) throws Exception { * Constructor. Properties can still be set before the wrapped Jetty HTTP * client is effectively created and configured via the * {@link #createHttpClient()} method. - * + * * @param client The client connector to help. */ public HttpClientHelper(Client client) { @@ -247,14 +237,15 @@ public HttpClientHelper(Client client) { getProtocols().add(Protocol.HTTP); getProtocols().add(Protocol.HTTPS); this.authenticationStore = null; - this.cookieStore = isCookieSupported() ? new HttpCookieStore.Default() + this.cookieStore = isCookieSupported() + ? new HttpCookieStore.Default() : new HttpCookieStore.Empty(); this.executor = null; } /** * Creates a low-level HTTP client call from a high-level uniform call. - * + * * @param request The high-level request. * @return A low-level HTTP client call. */ @@ -275,7 +266,7 @@ public ClientCall create(Request request) { /** * Creates a Jetty HTTP client. - * + * * @return A new HTTP client. */ protected HttpClient createHttpClient() { @@ -290,54 +281,31 @@ protected HttpClient createHttpClient() { } HttpClientTransport httpTransport = null; - HTTP2Client http2Client = null; - HTTP3Client http3Client = null; - - switch (getHttpClientTransportMode()) { - case "HTTP2": - http2Client = new HTTP2Client(); - HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2( - http2Client); - http2Transport.setUseALPN(true); - httpTransport = http2Transport; - break; - - case "HTTP3": - ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration( - sslContextFactory, null); - http3Client = new HTTP3Client(clientQuicConfig); - http3Client.getQuicConfiguration() - .setSessionRecvWindow(64 * 1024 * 1024); - httpTransport = new HttpClientTransportOverHTTP3(http3Client); - break; - - case "DYNAMIC": - ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; - - http2Client = new HTTP2Client(); - ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2( - http2Client); - - ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration( - sslContextFactory, null); - http3Client = new HTTP3Client(quicConfiguration); - ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3( - http3Client); - - HttpClientTransportDynamic httpDynamicTransport = new HttpClientTransportDynamic( - new ClientConnector(), http1, http2, http3); - httpTransport = httpDynamicTransport; - break; - - case "HTTP11": - default: - httpTransport = new HttpClientTransportOverHTTP(); - break; + + final String httpClientTransportMode = getHttpClientTransportMode(); + switch (httpClientTransportMode) { + case "HTTP2": + httpTransport = getHttpClientTransportForHttp2(); + break; + case "HTTP3": + httpTransport = getHttpClientTransportForHttp3(sslContextFactory); + break; + case "DYNAMIC": + httpTransport = getHttpClientTransportForDynamicMode(sslContextFactory); + break; + case "HTTP11": + httpTransport = getHttpTransportForHttp1_1(); + break; + default: + getLogger().log(Level.WARNING, + "Unknown HTTP client transport mode: {0}, use HTTP11 instead", httpClientTransportMode); + httpTransport = getHttpTransportForHttp1_1(); + break; } HttpClient httpClient = new HttpClient(httpTransport); httpClient.setAddressResolutionTimeout(getAddressResolutionTimeout()); - if(getAuthenticationStore() != null) { + if (getAuthenticationStore() != null) { httpClient.setAuthenticationStore(getAuthenticationStore()); } httpClient.setBindAddress(getBindAddress()); @@ -349,27 +317,25 @@ protected HttpClient createHttpClient() { switch (getHttpComplianceMode()) { - case "RFC7230": - httpClient.setHttpCompliance(HttpCompliance.RFC7230); - break; - case "RFC7230_LEGACY": - httpClient.setHttpCompliance(HttpCompliance.RFC7230_LEGACY); - break; - case "RFC2616": - httpClient.setHttpCompliance(HttpCompliance.RFC2616); - break; - case "RFC2616_LEGACY": - httpClient.setHttpCompliance(HttpCompliance.RFC2616_LEGACY); - break; + case "RFC7230": + httpClient.setHttpCompliance(HttpCompliance.RFC7230); + break; + case "RFC7230_LEGACY": + httpClient.setHttpCompliance(HttpCompliance.RFC7230_LEGACY); + break; + case "RFC2616": + httpClient.setHttpCompliance(HttpCompliance.RFC2616); + break; + case "RFC2616_LEGACY": + httpClient.setHttpCompliance(HttpCompliance.RFC2616_LEGACY); + break; } httpClient.setHttpCookieStore(getCookieStore()); httpClient.setIdleTimeout(getIdleTimeout()); - httpClient.setMaxConnectionsPerDestination( - getMaxConnectionsPerDestination()); + httpClient.setMaxConnectionsPerDestination(getMaxConnectionsPerDestination()); httpClient.setMaxRedirects(getMaxRedirects()); - httpClient.setMaxRequestsQueuedPerDestination( - getMaxRequestsQueuedPerDestination()); + httpClient.setMaxRequestsQueuedPerDestination(getMaxRequestsQueuedPerDestination()); httpClient.setMaxResponseHeadersSize(getMaxResponseHeadersSize()); String httpProxyHost = getProxyHost(); @@ -393,10 +359,44 @@ protected HttpClient createHttpClient() { return httpClient; } + private static HttpClientTransportOverHTTP getHttpTransportForHttp1_1() { + return new HttpClientTransportOverHTTP(); + } + + private static HttpClientTransport getHttpClientTransportForHttp2() { + HTTP2Client http2Client = new HTTP2Client(); + HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2(http2Client); + http2Transport.setUseALPN(true); + + return http2Transport; + } + + private static HttpClientTransport getHttpClientTransportForHttp3(SslContextFactory.Client sslContextFactory) { + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration); + http3Client.getQuicConfiguration().setSessionRecvWindow(64 * 1024 * 1024); + + return new HttpClientTransportOverHTTP3(http3Client); + } + + private static HttpClientTransport getHttpClientTransportForDynamicMode(SslContextFactory.Client sslContextFactory) { + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + + return new HttpClientTransportDynamic(new ClientConnector(), http1, http2, http3); + } + /** * The timeout in milliseconds for the DNS resolution of host addresses. * Defaults to 15000. - * + * * @return The address resolution timeout. */ public long getAddressResolutionTimeout() { @@ -406,16 +406,26 @@ public long getAddressResolutionTimeout() { /** * Returns the wrapped Jetty authentication store. - * + * * @return The wrapped Jetty authentication store. */ public AuthenticationStore getAuthenticationStore() { return authenticationStore; } + /** + * Sets the wrapped Jetty authentication store. + * + * @param authenticationStore The wrapped Jetty authentication store. + */ + public void setAuthenticationStore( + AuthenticationStore authenticationStore) { + this.authenticationStore = authenticationStore; + } + /** * The address to bind socket channels to. Default to null. - * + * * @return The bind address or null. */ public SocketAddress getBindAddress() { @@ -432,7 +442,7 @@ public SocketAddress getBindAddress() { /** * The max time in milliseconds a connection can take to connect to * destinations. Defaults to 15000. - * + * * @return The connect timeout. */ public long getConnectTimeout() { @@ -442,17 +452,26 @@ public long getConnectTimeout() { /** * Returns the wrapped Jetty cookie store. - * + * * @return The wrapped Jetty cookie store. */ public HttpCookieStore getCookieStore() { return this.cookieStore; } + /** + * Sets the wrapped Jetty cookie store. + * + * @param cookieStore The wrapped Jetty cookie store. + */ + public void setCookieStore(HttpCookieStore cookieStore) { + this.cookieStore = cookieStore; + } + /** * The timeout in milliseconds for idle destinations to be removed. Defaults * to 15000. - * + * * @return The address resolution timeout. */ public long getDestinationIdleTimeout() { @@ -461,18 +480,27 @@ public long getDestinationIdleTimeout() { } /** - * Returns the executor. By default returns an instance of + * Returns the executor. By default, returns an instance of * {@link QueuedThreadPool}. - * + * * @return Returns the executor. */ public Executor getExecutor() { return this.executor; } + /** + * Sets the executor. + * + * @param executor The executor. + */ + public void setExecutor(Executor executor) { + this.executor = executor; + } + /** * Returns the wrapped Jetty HTTP client. - * + * * @return The wrapped Jetty HTTP client. */ public HttpClient getHttpClient() { @@ -482,8 +510,8 @@ public HttpClient getHttpClient() { /** * Returns the HTTP compliance mode among the following options: "RFC7230", * "RFC2616", "LEGACY", "RFC7230_LEGACY". See {@link HttpCompliance}. - * Defaults to "RFC7230". - * + * Default to "RFC7230". + * * @return The HTTP compliance mode. */ public String getHttpComplianceMode() { @@ -495,7 +523,7 @@ public String getHttpComplianceMode() { * Returns the HTTP client transport mode among the following options: * "HTTP11", "HTTP2", "HTTP3", "DYNAMIC. See {@link HttpClientTransport}. * Defaults to "HTTP11". - * + * * @return The HTTP client transport mode. */ public String getHttpClientTransportMode() { @@ -506,7 +534,7 @@ public String getHttpClientTransportMode() { /** * The max time in milliseconds a connection can be idle (that is, without * traffic of bytes in either direction). Defaults to 60000. - * + * * @return The idle timeout. */ public long getIdleTimeout() { @@ -524,7 +552,7 @@ public long getIdleTimeout() { * load test), and it is recommended to set this value to a high value (at * least as much as the threads present in the {@link #getExecutor() * executor}). - * + * * @return The maximum connections per destination. */ public int getMaxConnectionsPerDestination() { @@ -534,7 +562,7 @@ public int getMaxConnectionsPerDestination() { /** * The max number of HTTP redirects that are followed. Defaults to 8. - * + * * @return The maximum redirects. */ public int getMaxRedirects() { @@ -553,7 +581,7 @@ public int getMaxRedirects() { * If this client is used for load testing, it is common to have this * parameter set to a high value, although this may impact latency (requests * sit in the queue for a long time before being sent). - * + * * @return The maximum requests queues per destination. */ public int getMaxRequestsQueuedPerDestination() { @@ -563,8 +591,8 @@ public int getMaxRequestsQueuedPerDestination() { /** * Returns the max size in bytes of the response headers. Default is -1 - * which is unlimited. - * + * that is unlimited. + * * @return the max size in bytes of the response headers. */ public int getMaxResponseHeadersSize() { @@ -574,7 +602,7 @@ public int getMaxResponseHeadersSize() { /** * Returns the host name of the HTTP proxy, if specified. - * + * * @return the host name of the HTTP proxy, if specified. */ public String getProxyHost() { @@ -584,7 +612,7 @@ public String getProxyHost() { /** * Returns the port of the HTTP proxy, if specified, 3128 otherwise. - * + * * @return the port of the HTTP proxy. */ public int getProxyPort() { @@ -594,7 +622,7 @@ public int getProxyPort() { /** * The size in bytes of the buffer used to write requests. Defaults to 4096. - * + * * @return The request buffer size. */ public int getRequestBufferSize() { @@ -605,7 +633,7 @@ public int getRequestBufferSize() { /** * The size in bytes of the buffer used to read responses. Defaults to * 16384. - * + * * @return The response buffer size. */ public int getResponseBufferSize() { @@ -616,7 +644,7 @@ public int getResponseBufferSize() { /** * The scheduler. Defaults to null. When null, creates a new instance of * {@link ScheduledExecutorScheduler}. - * + * * @return The scheduler. */ public Scheduler getScheduler() { @@ -625,8 +653,8 @@ public Scheduler getScheduler() { /** * The "User-Agent" HTTP header string. When null, uses the Jetty default. - * Defaults to null. - * + * Default to null. + * * @return The user agent field or null. */ public String getUserAgentField() { @@ -636,7 +664,7 @@ public String getUserAgentField() { /** * Indicates whether the connect operation is blocking. See * {@link HttpClient#isConnectBlocking()}. - * + * * @return True if the connect operation is blocking. */ public boolean isConnectBlocking() { @@ -647,7 +675,7 @@ public boolean isConnectBlocking() { /** * Whether to support cookies, storing and automatically sending them back. * Defaults to false. - * + * * @return Whether to support cookies. */ public boolean isCookieSupported() { @@ -657,7 +685,7 @@ public boolean isCookieSupported() { /** * Whether to follow HTTP redirects. Defaults to true. - * + * * @return Whether to follow redirects. */ public boolean isFollowRedirects() { @@ -686,7 +714,7 @@ public boolean isFollowRedirects() { * When not enforced, a "begin" event of a second request may happen before * the "complete" event of a first request and allow for better usage of * connections. - * + * * @return Whether request events must be strictly ordered. */ public boolean isStrictEventOrdering() { @@ -694,34 +722,6 @@ public boolean isStrictEventOrdering() { .getFirstValue("strictEventOrdering", "false")); } - /** - * Sets the wrapped Jetty authentication store. - * - * @param authenticationStore The wrapped Jetty authentication store. - */ - public void setAuthenticationStore( - AuthenticationStore authenticationStore) { - this.authenticationStore = authenticationStore; - } - - /** - * Sets the wrapped Jetty cookie store. - * - * @param cookieStore The wrapped Jetty cookie store. - */ - public void setCookieStore(HttpCookieStore cookieStore) { - this.cookieStore = cookieStore; - } - - /** - * Sets the executor. - * - * @param executor The executor. - */ - public void setExecutor(Executor executor) { - this.executor = executor; - } - @Override public void start() throws Exception { super.start(); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java index e0f0416061..4e4fb78749 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java @@ -66,16 +66,19 @@ public HttpsServerHelper(Server server) { @Override protected ConnectionFactory[] createConnectionFactories( HttpConfiguration configuration) { + ConnectionFactory[] result; + try { SslContextFactory.Server sslContextFactory = new RestletSslContextFactoryServer( org.restlet.engine.ssl.SslUtils.getSslContextFactory(this)); - return AbstractConnectionFactory.getFactories(sslContextFactory, + result = AbstractConnectionFactory.getFactories(sslContextFactory, new HttpConnectionFactory(configuration)); } catch (Exception e) { + result = null; getLogger().log(Level.WARNING, "Unable to create the Jetty SSL context factory", e); } - return null; + return result; } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java index dcacf062e8..d1feffb732 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java @@ -9,19 +9,10 @@ package org.restlet.ext.jetty; -import java.net.Socket; -import java.util.Arrays; -import java.util.concurrent.Executor; - import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.LowResourceMonitor; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; @@ -30,6 +21,10 @@ import org.restlet.Server; import org.restlet.ext.jetty.internal.JettyServerCall; +import java.net.Socket; +import java.util.Arrays; +import java.util.concurrent.Executor; + /** * Abstract Jetty web server connector. Here is the list of parameters that are * supported. They should be set in the Server's context before it is started: @@ -77,15 +72,14 @@ * connector.acceptors * int * -1 - * Connector acceptor thread count; when -1, Jetty will default to - * {@link Runtime#availableProcessors()} / 2, with a minimum of 1 + * Connector acceptor thread count; when -1, Jetty will default to 1 * * * connector.selectors * int * -1 - * Connector selector thread count; when -1, Jetty will default to - * {@link Runtime#availableProcessors()} + * Connector selector thread count; When less or equal than 0, + * Jetty computes a default value derived from a heuristic over available CPUs and thread pool size.} * * * connector.acceptQueueSize @@ -152,41 +146,66 @@ * lowResource.period * int * 1000 - * Low resource monitor period in milliseconds; when 0, low resource + * Low-resource monitor period in milliseconds; when 0, low-resource * monitoring is disabled * * * lowResource.threads * boolean * true - * Low resource monitor, whether to check if we're low on threads + * Low-resource monitor, whether to check if we're low on threads * * * lowResource.maxMemory * int * 0 - * Low resource monitor max memory in bytes; when 0, the check disabled; + * Low-resource monitor max memory in bytes; when 0, the check disabled; * memory used is calculated as (totalMemory-freeMemory) * * * lowResource.maxConnections * int * 0 - * Low resource monitor max connections; when 0, the check is disabled + * Low-resource monitor max connections; when 0, the check is disabled. Deprecated, see server.maxConnections * * * lowResource.idleTimeout * int * 1000 - * Low resource monitor idle timeout in milliseconds; applied to EndPoints - * when in the low resources state + * Low-resource monitor idle timeout in milliseconds; applied to EndPoints + * when in the low-resources state * * * lowResource.stopTimeout * long * 30000 - * Low resource monitor stop timeout in milliseconds; the maximum time - * allowed for the service to shutdown + * Low-resource monitor stop timeout in milliseconds; the maximum time + * allowed for the service to shutdown. Deprecated, use shutdown.timeout instead + * + * + * server.maxConnections + * int + * 0 + * Server max connections; when 0, there is no limit + * + * + * server.maxConnections.idleTimeout + * long + * 0 + * The endpoint idle timeout in milliseconds to apply when the connection limit is reached; when 0, there is no idle timeout. + * + * + * shutdown.gracefully + * boolean + * true + * When true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending + * requests to end before shutting down. Otherwise, incoming requests are not blocked and all requests are aborted. + * + * + * shutdown.timeout + * long + * 30000 + * Server shutdown timeout in milliseconds. Defaults to 30000. * * * @@ -199,45 +218,6 @@ public abstract class JettyServerHelper extends org.restlet.engine.adapter.HttpServerHelper { - /** - * Jetty server wrapped by a parent Restlet HTTP server connector. - * - * @author Jerome Louvel - * @author Tal Liron - */ - private static class WrappedServer extends org.eclipse.jetty.server.Server { - private final JettyServerHelper serverHelper; - - /** - * Constructor. - * - * @param serverHelper The Jetty HTTP server. - * @param threadPool The thread pool. - */ - public WrappedServer(JettyServerHelper serverHelper, - ThreadPool threadPool) { - super(threadPool); - this.serverHelper = serverHelper; - } - - /** - * Handler method converting a Jetty call into a Restlet Call. - * - * @param request The Jetty request to handle. - * @param response The Jetty response to handle. - * @param callback The Jetty callback to use if needed. - * @return True if processing was successful. - */ - @Override - public boolean handle(Request request, Response response, - Callback callback) throws Exception { - this.serverHelper - .handle(new JettyServerCall(this.serverHelper.getHelped(), - request, response, callback)); - return true; - } - } - /** The wrapped Jetty server. */ private volatile org.eclipse.jetty.server.Server wrappedServer; @@ -286,6 +266,7 @@ protected abstract ConnectionFactory[] createConnectionFactories( */ private Connector createConnector(org.eclipse.jetty.server.Server server) { final HttpConfiguration configuration = createConfiguration(); + final ConnectionFactory[] connectionFactories = createConnectionFactories( configuration); @@ -300,44 +281,42 @@ private Connector createConnector(org.eclipse.jetty.server.Server server) { connectionFactories); final String address = getHelped().getAddress(); - if (address != null) + if (address != null) { connector.setHost(address); + } connector.setPort(getHelped().getPort()); connector.setAcceptQueueSize(getConnectorAcceptQueueSize()); connector.setIdleTimeout(getConnectorIdleTimeout()); -// connector.setSoLingerTime(getConnectorSoLingerTime()); -// connector.setStopTimeout(getConnectorStopTimeout()); + connector.setShutdownIdleTimeout(getShutdownTimeout()); + // connector.setSoLingerTime(getConnectorSoLingerTime()); return connector; } /** - * Creates a Jetty low resource monitor. + * Creates a Jetty low-resource monitor. * * @param server A Jetty server. - * @return A Jetty low resource monitor or null. + * @return A Jetty low-resource monitor or null. */ private LowResourceMonitor createLowResourceMonitor( org.eclipse.jetty.server.Server server) { + final LowResourceMonitor result; + final int period = getLowResourceMonitorPeriod(); if (period > 0) { - final LowResourceMonitor lowResourceMonitor = new LowResourceMonitor( - server); - lowResourceMonitor.setMonitoredConnectors( - Arrays.asList(server.getConnectors())); - lowResourceMonitor.setPeriod(period); - lowResourceMonitor - .setMonitorThreads(getLowResourceMonitorThreads()); - lowResourceMonitor.setMaxMemory(getLowResourceMonitorMaxMemory()); -// lowResourceMonitor.setMaxConnections(getLowResourceMonitorMaxConnections()); - lowResourceMonitor.setLowResourcesIdleTimeout( - getLowResourceMonitorIdleTimeout()); -// lowResourceMonitor.setStopTimeout(getLowResourceMonitorStopTimeout()); - server.addBean(lowResourceMonitor); - return lowResourceMonitor; + result = new LowResourceMonitor(server); + result.setMonitoredConnectors(Arrays.asList(server.getConnectors())); + result.setPeriod(period); + result.setMonitorThreads(getLowResourceMonitorThreads()); + result.setMaxMemory(getLowResourceMonitorMaxMemory()); + result.setLowResourcesIdleTimeout(getLowResourceMonitorIdleTimeout()); + } else { + result = null; } - return null; + + return result; } /** @@ -350,18 +329,64 @@ private org.eclipse.jetty.server.Server createServer() { final ThreadPool threadPool = createThreadPool(); // Server - final org.eclipse.jetty.server.Server server = new WrappedServer(this, - threadPool); + final org.eclipse.jetty.server.Server jettyServer = new org.eclipse.jetty.server.Server(threadPool); - // Connector - final Connector connector = createConnector(server); - server.addConnector(connector); + int serverMaxConnections = getServerMaxConnections(); + if (serverMaxConnections > 0) { + ConnectionLimit connectionLimit = new ConnectionLimit(serverMaxConnections, jettyServer); + connectionLimit.setIdleTimeout(getServerMaxConnectionsIdleTimeout()); + jettyServer.addBean(connectionLimit); + } + + jettyServer.setStopAtShutdown(getShutdownGracefully()); + if (getShutdownGracefully()) { + jettyServer.setStopTimeout(getShutdownTimeout()); + } else { + jettyServer.setStopTimeout(0); + } - // Low resource monitor (must be created after connectors have been - // added) - createLowResourceMonitor(server); + jettyServer.setHandler(createJettyHandler()); - return server; + // Connector + final Connector connector = createConnector(jettyServer); + jettyServer.addConnector(connector); + + // Low-resource monitor (must be created after connectors have been added) + LowResourceMonitor lowResourceMonitor = createLowResourceMonitor(jettyServer); + jettyServer.addBean(lowResourceMonitor); + + return jettyServer; + } + + /** + * Creates the Jetty handler that wraps the {@link JettyServerHelper}. + * + * @return the Jetty handler that wraps the {@link JettyServerHelper}. + */ + private Handler.Abstract createJettyHandler() { + final Handler.Abstract result; + + final JettyServerHelper jettyServerHelper = this; + Handler.Abstract jettyServerHelperWrapperHandler = new Handler.Abstract() { + @Override + public boolean handle(Request request, Response response, Callback callback) { + JettyServerCall httpCall = new JettyServerCall(jettyServerHelper.getHelped(), + request, response, callback); + jettyServerHelper.handle(httpCall); + return true; // Indicates that the request is accepted + }; + }; + + if (getShutdownGracefully()) { + // StatisticsHandler for graceful shutdown + final StatisticsHandler statisticsHandler = new StatisticsHandler(); + statisticsHandler.setHandler(jettyServerHelperWrapperHandler); + result = statisticsHandler; + } else { + result = jettyServerHelperWrapperHandler; + } + + return result; } /** @@ -375,14 +400,18 @@ private ThreadPool createThreadPool() { threadPool.setMaxThreads(getThreadPoolMaxThreads()); threadPool.setThreadsPriority(getThreadPoolThreadsPriority()); threadPool.setIdleTimeout(getThreadPoolIdleTimeout()); - threadPool.setStopTimeout(getThreadPoolStopTimeout()); + if (getShutdownGracefully()) { + threadPool.setStopTimeout(getThreadPoolStopTimeout()); + } else { + threadPool.setStopTimeout(0); // The thread pool stops immediately. + } + return threadPool; } /** * Connector acceptor thread count. Defaults to -1. When -1, Jetty will - * default to {@link Runtime#availableProcessors()} / 2, with a minimum of - * 1. + * default to 1. * * @return Connector acceptor thread count. */ @@ -392,9 +421,10 @@ public int getConnectorAcceptors() { } /** - * Connector accept queue size. Defaults to 0. + * Connector "accept" queue size. + * Defaults to 0. *

- * Also known as accept backlog. + * Also known as "accept" backlog. * * @return Connector accept queue size. */ @@ -450,8 +480,10 @@ public Scheduler getConnectorScheduler() { } /** - * Connector selector thread count. Defaults to -1. When 0, Jetty will - * default to {@link Runtime#availableProcessors()}. + * Connector selector thread count. + * Defaults to -1. + * When less or equal than 0, + * Jetty computes a default value derived from a heuristic over available CPUs and thread pool size. * * @return Connector acceptor thread count. */ @@ -476,10 +508,12 @@ public int getConnectorSoLingerTime() { /** * Connector stop timeout in milliseconds. Defaults to 30000. *

- * The maximum time allowed for the service to shutdown. + * The maximum time allowed for the service to shut down. * * @return Connector stop timeout. + * @deprecated cf {@link #getShutdownTimeout()} */ + @Deprecated public int getConnectorStopTimeout() { return Integer.parseInt(getHelpedParameters() .getFirstValue("connector.stopTimeout", "30000")); @@ -492,14 +526,14 @@ public int getConnectorStopTimeout() { */ public int getHttpHeaderCacheSize() { return Integer.parseInt(getHelpedParameters() - .getFirstValue("http.headerCacheSize", "512")); + .getFirstValue("http.headerCacheSize", "1024")); } /** * HTTP output buffer size in bytes. Defaults to 32*1024. *

* A larger buffer can improve performance by allowing a content producer to - * run without blocking, however larger buffers consume more memory and may + * run without blocking, however, larger buffers consume more memory and may * induce some latency before a client starts processing the content. * * @return HTTP output buffer size. @@ -527,7 +561,7 @@ public int getHttpRequestHeaderSize() { * HTTP response header size in bytes. Defaults to 8*1024. *

* Larger headers will allow for more and/or larger cookies and longer HTTP - * headers (e.g. for redirection). However, larger headers will also consume + * headers (e.g., for redirection). However, larger headers will also consume * more memory. * * @return HTTP response header size. @@ -538,11 +572,12 @@ public int getHttpResponseHeaderSize() { } /** - * Low resource monitor idle timeout in milliseconds. Defaults to 1000. + * Low-resource monitor idle timeout in milliseconds. + * Defaults to 1000. *

- * Applied to EndPoints when in the low resources state. + * Applied to EndPoints when in the low-resources state. * - * @return Low resource monitor idle timeout. + * @return Low-resource monitor idle timeout. */ public int getLowResourceMonitorIdleTimeout() { return Integer.parseInt(getHelpedParameters() @@ -550,23 +585,27 @@ public int getLowResourceMonitorIdleTimeout() { } /** - * Low resource monitor max connections. Defaults to 0. When 0, the check is - * disabled. + * Low-resource monitor max connections. + * Defaults to 0. + * When 0, the check is disabled. * - * @return Low resource monitor max connections. + * @return Low-resource monitor max connections. + * @deprecated cf {@link #getServerMaxConnections()} */ + @Deprecated public int getLowResourceMonitorMaxConnections() { return Integer.parseInt(getHelpedParameters() .getFirstValue("lowResource.maxConnections", "0")); } /** - * Low resource monitor max memory in bytes. Defaults to 0. When 0, the - * check disabled. + * Low-resource monitor max memory in bytes. + * Defaults to 0. + * When 0, the check is disabled. *

* Memory used is calculated as (totalMemory-freeMemory). * - * @return Low resource monitor max memory. + * @return Low-resource monitor max memory. */ public long getLowResourceMonitorMaxMemory() { return Long.parseLong(getHelpedParameters() @@ -574,10 +613,11 @@ public long getLowResourceMonitorMaxMemory() { } /** - * Low resource monitor period in milliseconds. Defaults to 1000. When 0, - * low resource monitoring is disabled. + * Low-resource monitor period in milliseconds. + * Defaults to 1000. + * When 0, low-resource monitoring is disabled. * - * @return Low resource monitor period. + * @return Low-resource monitor period. */ public int getLowResourceMonitorPeriod() { return Integer.parseInt(getHelpedParameters() @@ -585,28 +625,99 @@ public int getLowResourceMonitorPeriod() { } /** - * Low resource monitor stop timeout in milliseconds. Defaults to 30000. + * Low-resource monitor stop timeout in milliseconds. + * Defaults to 30000. *

- * The maximum time allowed for the service to shutdown. + * The maximum time allowed for the service to shut down. * - * @return Low resource monitor stop timeout. + * @return Low-resource monitor stop timeout. + * @deprecated cf {@link #getShutdownTimeout()} */ + @Deprecated public long getLowResourceMonitorStopTimeout() { return Long.parseLong(getHelpedParameters() .getFirstValue("lowResource.stopTimeout", "30000")); } /** - * Low resource monitor, whether to check if we're low on threads. Defaults + * Low-resource monitor, whether to check if we're low on threads. Defaults * to true. * - * @return Low resource monitor threads. + * @return Low-resource monitor threads. */ public boolean getLowResourceMonitorThreads() { return Boolean.parseBoolean(getHelpedParameters() .getFirstValue("lowResource.threads", "true")); } + /** + * Server max connections. + * Defaults to 0. + * When 0, the check is disabled. + * + * @return Low-resource monitor max connections. + */ + public int getServerMaxConnections() { + final int result; + + final String serverMaxConnectionsValue = getHelpedParameters().getFirstValue("server.maxConnections"); + + if (serverMaxConnectionsValue == null) { + result = getLowResourceMonitorMaxConnections(); + } else { + result = Integer.parseInt(serverMaxConnectionsValue); + } + + return result; + } + + /** + * The endpoint idle timeout in milliseconds to apply when the connection limit is reached. + * Defaults to 0. + * When 0, there is no idle timeout. + *

+ * The maximum time allowed for the endpoint to close when the connection limit is reached. + * + * @return The endpoint idle timeout in milliseconds to apply when the connection limit is reached. + */ + public long getServerMaxConnectionsIdleTimeout() { + return Long.parseLong(getHelpedParameters() + .getFirstValue("server.maxConnections.idleTimeout", "0")); + } + + /** + * When true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending + * requests to end before shutting down. + * Otherwise, incoming requests are not blocked and all requests are aborted. + * Defaults to true. + * @return True if upon JVM shutdown, the Jetty server will block incoming requests and wait for pending requests to + * end before shutting down. + * Otherwise, incoming requests are not blocked and all requests are aborted. + */ + public boolean getShutdownGracefully() { + return Boolean.parseBoolean(getHelpedParameters().getFirstValue( + "shutdown.gracefully", "true")); + } + + /** + * Server shutdown timeout in milliseconds. Defaults to 30000. + * + * @return Server shutdown timeout. + */ + public long getShutdownTimeout() { + final long result; + + final String shutdownTimeoutValue = getHelpedParameters().getFirstValue("shutdown.timeout"); + + if (shutdownTimeoutValue == null) { + result = getLowResourceMonitorStopTimeout(); + } else { + result = Long.parseLong(shutdownTimeoutValue); + } + + return result; + } + /** * Thread pool idle timeout in milliseconds. Defaults to 60000. *

@@ -642,7 +753,7 @@ public int getThreadPoolMinThreads() { /** * Thread pool stop timeout in milliseconds. Defaults to 5000. *

- * The maximum time allowed for the service to shutdown. + * The maximum time allowed for the service to shut down. * * @return Thread pool stop timeout. */ @@ -668,8 +779,9 @@ public int getThreadPoolThreadsPriority() { * @return The wrapped Jetty server. */ protected org.eclipse.jetty.server.Server getWrappedServer() { - if (this.wrappedServer == null) + if (this.wrappedServer == null) { this.wrappedServer = createServer(); + } return this.wrappedServer; } @@ -678,8 +790,7 @@ protected org.eclipse.jetty.server.Server getWrappedServer() { * * @param wrappedServer The wrapped Jetty server. */ - protected void setWrappedServer( - org.eclipse.jetty.server.Server wrappedServer) { + protected void setWrappedServer(org.eclipse.jetty.server.Server wrappedServer) { this.wrappedServer = wrappedServer; } @@ -692,15 +803,15 @@ public void start() throws Exception { + " server on port " + getHelped().getPort()); try { server.start(); + // We won't know the local port until after the server starts + setEphemeralPort(connector.getLocalPort()); } catch (Exception e) { - // Make sure that all resources are released, otherwise threadpool + // Make sure that all resources are released, otherwise thread-pool // may still be running. server.stop(); throw e; } - // We won't know the local port until after the server starts - setEphemeralPort(connector.getLocalPort()); } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java index 5e41ad3382..ecbe2033d0 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java @@ -20,7 +20,7 @@ /** * Jetty handler that knows how to convert Jetty calls into Restlet calls. This - * handler isn't a full server, if you use it you need to manually setup the + * handler isn't a full server, if you use it, you need to manually set up the * Jetty server connector and add this handler to a Jetty server. * * @author Valdis Rigdon @@ -48,10 +48,11 @@ public JettyHandler(Server server) { * @param secure Indicates if the server supports HTTP or HTTPS. */ public JettyHandler(Server server, boolean secure) { - if (secure) + if (secure) { this.helper = new HttpsServerHelper(server); - else + } else { this.helper = new HttpServerHelper(server); + } } @Override @@ -77,8 +78,9 @@ protected void doStop() throws Exception { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - this.helper.handle(new JettyServerCall(this.helper.getHelped(), request, - response, callback)); + JettyServerCall httpCall = new JettyServerCall(this.helper.getHelped(), + request, response, callback); + this.helper.handle(httpCall); return true; } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java index d95e9bc465..8d81593a42 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java @@ -9,13 +9,6 @@ package org.restlet.ext.jetty.internal; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.cert.Certificate; -import java.util.Arrays; -import java.util.List; - import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; @@ -29,6 +22,13 @@ import org.restlet.engine.adapter.ServerCall; import org.restlet.util.Series; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.List; + /** * Call that is used by the Jetty HTTP server connectors. * @@ -93,12 +93,20 @@ public Callback getCallback() { @Override public List getCertificates() { + final List result; + if (getEndPoint() instanceof SslEndPoint sslEndPoint) { - return Arrays.asList(sslEndPoint.getSslSessionData() - .peerCertificates()); - } else { - return null; + if (sslEndPoint.getSslSessionData() != null + && sslEndPoint.getSslSessionData().peerCertificates() != null) { + result = Arrays.asList(sslEndPoint.getSslSessionData().peerCertificates()); + } else { + result = null; + } + } else { + result = null; } + + return result; } /** diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java index 6b940e8a00..0aa811c5aa 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java @@ -52,11 +52,10 @@ public SSLEngine newSSLEngine(String host, int port) { @Override public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException { - SSLServerSocketFactory factory = getSslContext() - .getServerSocketFactory(); - return (SSLServerSocket) ((host == null) ? factory.createServerSocket( - port, backlog) : factory.createServerSocket(port, backlog, - InetAddress.getByName(host))); + SSLServerSocketFactory factory = getSslContext().getServerSocketFactory(); + return (SSLServerSocket) ((host == null) + ? factory.createServerSocket(port, backlog) : + factory.createServerSocket(port, backlog, InetAddress.getByName(host))); } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java index 5a70a8e46d..589a41846b 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java @@ -52,11 +52,10 @@ public SSLEngine newSSLEngine(String host, int port) { @Override public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException { - SSLServerSocketFactory factory = getSslContext() - .getServerSocketFactory(); - return (SSLServerSocket) ((host == null) ? factory.createServerSocket( - port, backlog) : factory.createServerSocket(port, backlog, - InetAddress.getByName(host))); + SSLServerSocketFactory factory = getSslContext().getServerSocketFactory(); + return (SSLServerSocket) ((host == null) + ? factory.createServerSocket(port, backlog) + : factory.createServerSocket(port, backlog, InetAddress.getByName(host))); } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java new file mode 100644 index 0000000000..ec273f2de0 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java @@ -0,0 +1,38 @@ +package org.restlet.ext.jetty; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CountDownLatch; +import java.util.logging.Logger; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class Lock { + private static final Logger LOGGER = Logger.getLogger("Lock"); + + private final CountDownLatch lock = new CountDownLatch(1); + private final String name; + + public Lock(String name) { + this.name = name; + } + + public void unlock() { + log("lock " + name + " unlock"); + lock.countDown(); + } + + public boolean awaitForUnlockingFor(final Duration waitTime) throws InterruptedException { + log("lock " + name + " awaitForUnlocking"); + + final long waitTimeInMs = waitTime.toMillis(); + boolean await = lock.await(waitTimeInMs, MILLISECONDS); + log("lock " + name + " awaitForUnlocking done " + await); + return await; + } + + private static void log(final String message) { + LOGGER.fine(Instant.now().toString() + " " + message); + } + +} diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java new file mode 100644 index 0000000000..85c13e4171 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java @@ -0,0 +1,367 @@ +/** + * Copyright 2005-2014 Restlet + * + * The contents of this file are subject to the terms of one of the following + * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can + * select the license that you prefer but you may not use this file except in + * compliance with one of these Licenses. + * + * You can obtain a copy of the Apache 2.0 license at + * http://www.opensource.org/licenses/apache-2.0 + * + * You can obtain a copy of the EPL 1.0 license at + * http://www.opensource.org/licenses/eclipse-1.0 + * + * See the Licenses for the specific language governing permissions and + * limitations under the Licenses. + * + * Alternatively, you can obtain a royalty free commercial license with less + * limitations, transferable or non-transferable, directly at + * http://restlet.com/products/restlet-framework + * + * Restlet is a registered trademark of Restlet S.A.S. + */ + +package org.restlet.ext.jetty; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.restlet.*; +import org.restlet.data.MediaType; +import org.restlet.data.Protocol; +import org.restlet.data.Status; +import org.restlet.engine.Engine; +import org.restlet.resource.ClientResource; + +import java.time.Duration; +import java.time.Instant; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ShutdownHookTestCase { + private static final Logger LOGGER = Logger.getLogger("ShutdownHookTest"); + private static boolean shouldDebug = false; + + @BeforeAll + private static void setUp() { + LOGGER.setLevel(Level.INFO); + } + + /** + * Validates that the server stops immediately when no requests are currently handled. + */ + @Test + public void whenServerIsNotHandlingRequestThenItStopsImmediately() throws Exception { + // Given a server resource that takes 1 min to send a response + final Restlet hangingRestlet = newHangingRestlet(Duration.ofMinutes(1)); + // Given a server with a 3-seconds graceful shutdown + final Server server = startServerWithGracefulShutdown(Duration.ofSeconds(3), hangingRestlet); + + // When the server stops + final Instant serverAskedToStopInstant = stopServer(server); + + // Then the server stops immediately (no pending request) + assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that hanging requests are aborted immediately when graceful shutdown is OFF. + * + * This is done by making a request froze, then stopping the server, and checking that it didn't wait. + */ + @Test + public void whenServerIsHandlingBlockingRequestThenItStopsImmediately() throws Exception { + // Given a server resource that takes 1 min to send a response + final Lock lock = new Lock("Server"); + final Restlet hangingRestlet = newHangingAndLockedRestlet(Duration.ofMinutes(1), lock); + // Given a server without graceful shutdown + Server server = startServerWithoutGracefulShutdown(hangingRestlet); + + // Given a client that sends a request + final TestClient testClient = new TestClient(server); + new Thread(testClient).start(); + + // When we stop the server while there is a pending request + log("before resource unlock"); + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(Duration.ofSeconds(3)); + log("after resource unlock"); + log("before stopping server while request is pending"); + final Instant serverAskedToStopInstant = stopServer(server); + log("before client unlock"); + final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(Duration.ofSeconds(4)); + log("after client unlock"); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isClientResourceUnlocked, "The client didn't achieve the request"); + assertTrue(testClient.cr.getStatus().isError(), "The request should have ended in error"); + assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, testClient.stoppedAt); + assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that hanging requests are aborted after the server has waited for the timeout when graceful shutdown is ON. + * + * This is done by making a request froze, then stopping the server, and checking that it waited the expected amount of time before shutting down. + */ + @Test + public void whenServerIsHandlingBlockingRequestThenItGracefullyWaitsFor1SecondBeforeStopping() throws Exception { + // Given a server resource that takes 1 min to send a response + final Lock serverLock = new Lock("Server"); + final Restlet hangingRestlet = newHangingAndLockedRestlet(Duration.ofMinutes(1), serverLock); + + // Given a server with a 1-second graceful shutdown + final Duration shutdownTimeout = Duration.ofSeconds(1); + final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + + // Given a client that sends a request + final TestClient hangingClient = new TestClient(server); + new Thread(hangingClient).start(); + + // When we stop the server while there is a pending request + final boolean isResourceUnlocked = serverLock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + log("Before ask server to stop"); + final Instant serverAskedToStopInstant = stopServer(server); + log("After ask server to stop"); + final boolean isClientResourceUnlocked = hangingClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isClientResourceUnlocked, "The client didn't achieved the request"); + assertTrue(hangingClient.cr.getStatus().isError(), "The request should have ended in error"); + + assertIntervalBetweenDatesEquals(shutdownTimeout, serverAskedToStopInstant, hangingClient.stoppedAt); + assertIntervalBetweenDatesEquals(toJettyEffectiveTimeout(shutdownTimeout), serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that incoming requests are refused after the server is stopping when graceful shutdown is ON. + * + * This is done by making a request froze, then stopping the server, and checking that a new request is not taken into account. + */ + @Test + public void whenServerIsHandlingBlockingRequestThenItRefusesNewRequest() throws Exception { + // Given a server resource that takes 1 min to send a response + final Lock lock = new Lock("Server"); + final Restlet hangingRestlet = newHangingAndLockedRestlet(Duration.ofMinutes(1), lock); + + // Given a server with a 1-second graceful shutdown + final Duration shutdownTimeout = Duration.ofSeconds(1); + final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + + // Given a client that sends a request + final TestClient firstTestClient = new TestClient(server); + new Thread(firstTestClient).start(); + + // When + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + final Instant serverAskedToStopInstant = stopServer(server); + final TestClient blockedTestClient = new TestClient(server); + blockedTestClient.run(); + final boolean isFirstClientResourceUnlocked = firstTestClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + final boolean isBlockedClientResourceUnlocked = blockedTestClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isFirstClientResourceUnlocked, "The first client didn't achieved the request"); + assertTrue(isBlockedClientResourceUnlocked, "The \"blocked\" client didn't achieved the request"); + assertTrue(firstTestClient.cr.getStatus().isError(), "The request should have ended in error"); + assertIntervalBetweenDatesEquals(shutdownTimeout, serverAskedToStopInstant, firstTestClient.stoppedAt); + assertTrue(blockedTestClient.cr.getStatus().isConnectorError(), "Any new client is blocked and fails with a connection error"); + assertIntervalBetweenDatesEquals(toJettyEffectiveTimeout(shutdownTimeout), serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that hanging requests for a short amount of time are handled before the server has waited for the timeout when graceful shutdown is ON. + * + * This is done by making a request froze for a short amount of time, then stopping the server, and checking that the request has been handled. + */ + @Test + public void whenServerIsHandlingLongRequestThenRequestIsHandledCorrectlyBeforeStopping() throws Exception { + // Given a server resource that takes 1 sec to send a response + final Lock lock = new Lock("Server"); + Duration requestHangingTime = Duration.ofSeconds(1); + final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + + // Given a server with a 20-seconds graceful shutdown + final Duration shutdownTimeout = Duration.ofSeconds(20); + final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + + // Given a client that sends a request + final TestClient testClient = new TestClient(server); + new Thread(testClient).start(); + + // When + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + final Instant serverAskedToStopInstant = stopServer(server); + log("Client resource wait lock"); + final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + log("Client resource unlocked"); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isClientResourceUnlocked, "The client didn't achieve the request"); + assertEquals(Status.SUCCESS_OK, testClient.cr.getStatus()); + assertIntervalBetweenDatesIsLessThan(requestHangingTime, serverAskedToStopInstant, Instant.now()); + assertEquals("hello, world", testClient.responseText); + } + + private static void assertIntervalBetweenDatesEquals(final Duration expectedDuration, final Instant firstInstant, final Instant secondInstant) { + final Duration dateDifference = Duration.between(firstInstant, secondInstant) + .abs(); + + final Duration tolerance = Duration.ofMillis(200); // let's consider that +/- 200ms is fine + final boolean isDateDifferenceNearlyEqualToExpectedDuration = dateDifference.minus(expectedDuration) + .abs() + .minus(tolerance) + .isNegative(); + assertTrue(isDateDifferenceNearlyEqualToExpectedDuration, String.format("Expected delay: %d second(s) versus %d second(s)\n", expectedDuration.toMillis(), dateDifference.toMillis())); + } + + private static void assertIntervalBetweenDatesIsLessThan(final Duration expectedDuration, final Instant firstInstant, final Instant secondInstant) { + final Duration dateDifference = Duration.between(firstInstant, secondInstant).abs(); + + final Duration tolerance = Duration.ofMillis(200); // let's consider that +/- 200ms is fine + final boolean dateDifferenceIsLessThanExpectedDuration = dateDifference.minus(expectedDuration) + .abs() + .minus(tolerance) + .isNegative(); + assertTrue(dateDifferenceIsLessThanExpectedDuration, String.format("difference between dates [%d second(s)] is higher than expected: %d second(s)\n", dateDifference.getSeconds(), expectedDuration.getSeconds())); + } + + private Server startServerWithoutGracefulShutdown(final Restlet restlet) throws Exception { + return startServer(false, Duration.ZERO, restlet); + } + + private Server startServerWithGracefulShutdown(final Duration timeout, final Restlet restlet) throws Exception { + return startServer(true, timeout, restlet); + } + + private Server startServer(final boolean graceful, final Duration timeout, final Restlet restlet) throws Exception { + + Engine.getInstance().getRegisteredServers().add(0, new HttpServerHelper(null)); // Creates a Jetty server helper manually + // 0 port means it will be computed when the server starts + Server server = new Server(new Context(), singletonList(Protocol.HTTP), null, 0, restlet, HttpServerHelper.class.getCanonicalName()); + + if (shouldDebug) { + server.getContext().getParameters().add("tracing", "true"); + System.setProperty("org.eclipse.jetty.LEVEL", "TRACE"); + System.setProperty("sun.net.www.protocol.http.HttpURLConnection.LEVEL", "ALL"); + Engine.setLogLevel(Level.FINE); + } + + if (graceful) { + // Don't let the lowResource monitor mess with the current test + server.getContext().getParameters().add("lowResource.idleTimeout", Long.toString(timeout.toMillis() * 10)); + } + server.getContext().getParameters().add("shutdown.gracefully", Boolean.toString(graceful)); + server.getContext().getParameters().add("shutdown.timeout", Long.toString(timeout.toMillis())); + + server.start(); + log( "Server started on port " + server.getEphemeralPort()); + return server; + } + + /** + * Creates a resource that hangs for a specific amount of time before answering and acknowledges incoming requests + * by unlocking the given lock. + */ + private static Restlet newHangingAndLockedRestlet(final Duration requestHangingTime, final Lock lock) { + return new Restlet() { + @Override + public void handle(final Request request, final Response response) { + log("Restlet opens lock"); + lock.unlock(); + log("Restlet starts sleeping"); + try { + Thread.sleep(requestHangingTime.toMillis()); + } catch (Exception e) { + // silently stops, especially when Jetty server will abruptly quit after time out + LOGGER.log(Level.FINE, "Restlet error", e); + } + log("Restlet woke up, answering"); + response.setEntity("hello, world", MediaType.TEXT_ALL); + } + }; + } + + /** + * Creates a resource that hangs for a specific amount of time before answering. + */ + private static Restlet newHangingRestlet(final Duration requestHangingTime) { + return new Restlet() { + @Override + public void handle(final Request request, final Response response) { + try { + Thread.sleep(requestHangingTime.toMillis()); + } catch (Exception e) { + // silently stops, especially when Jetty server will abruptly quit after time out + LOGGER.log(Level.FINE, "Restlet error", e); + } + LOGGER.log(Level.FINE, "Restlet woke up, answering"); + response.setEntity("hello, world", MediaType.TEXT_ALL); + } + }; + } + + private static class TestClient implements Runnable { + private final ClientResource cr; + private Instant stoppedAt = null; + private final Lock lock; // ensures that the client has totally run + private String responseText; + + public TestClient(final Server server) { + cr = new ClientResource("http://localhost:" + server.getEphemeralPort()); + cr.setRetryOnError(false); + this.lock = new Lock("TestClient"); + } + + @Override + public void run() { + try { + LOGGER.log(Level.FINE, "Client running"); + responseText = cr.get().getText(); + } catch (Exception e) { // silently ignore errors + LOGGER.log(Level.FINE, "Client error", e); + } finally { + stoppedAt = Instant.now(); + lock.unlock(); + LOGGER.log(Level.FINE, "Client state: " + cr.getStatus()); + } + } + } + + private synchronized Instant stopServer(final Server server) { + log("Server stopping"); + Instant serverAskedToStopInstant = Instant.now(); + try { + final HttpServerHelper serverHelper = (HttpServerHelper) server.getContext().getAttributes().get("org.restlet.engine.helper"); + serverHelper.getWrappedServer().stop(); + log("Server stopped"); + } catch (Exception e) { + // silently ignore errors + log("Server stopped", e); + } + return serverAskedToStopInstant; + } + + /** + * Returns the effective Jetty timeout since there is an extra half-timeout in the {@link org.eclipse.jetty.util.thread.QueuedThreadPool}. + */ + private Duration toJettyEffectiveTimeout(final Duration timeout) { + return timeout.plusMillis(500); // FIXME: needs improvements + } + + private static void log(final String message) { + LOGGER.info(Instant.now().toString() + " " + message); + } + + private static void log(final String message, final Exception exception) { + LOGGER.log(Level.INFO, Instant.now().toString() + " " + message, exception); + } + +} diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java index c263d83328..0eed13fe13 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java @@ -11,15 +11,16 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.restlet.Application; -import org.restlet.Component; -import org.restlet.Server; +import org.restlet.*; import org.restlet.data.Protocol; import org.restlet.engine.Engine; import org.restlet.engine.adapter.HttpServerHelper; import org.restlet.engine.connector.ClientHelper; +import org.restlet.engine.connector.HttpClientHelper; import org.restlet.engine.connector.ServerHelper; +import java.util.List; +import java.util.logging.Level; import java.util.stream.Stream; import static org.junit.jupiter.api.DynamicTest.dynamicTest; @@ -32,57 +33,103 @@ * @author Jerome Louvel */ public abstract class BaseConnectorsTestCase { - private Component component; private int port; - protected abstract void doTestUri(String uri) throws Exception; + /** + * The method to implement in the tests + * + * @param serverPort The port that the server is listening to. + * @throws Exception + */ + protected abstract void doTest(final int serverPort) throws Exception; + + protected boolean shouldDebug() { + return false; + } - protected Server configureServer(final Component component) { - // server.getContext().getParameters().add("tracing", "true"); + protected Server createServer(final Component component) { return component.getServers().add(Protocol.HTTP, 0); } - protected abstract Application createApplication(); + protected void configureServer(final Server server) { + server.getContext().getParameters().add("threadPool.minThreads", "1"); + server.getContext().getParameters().add("threadPool.maxThreads", "10"); + server.getContext().getParameters().add("shutdown.gracefully", "false"); - protected String getCallUri(final int port) { - return "http://localhost:" + port + "/test"; + if (shouldDebug()) { + server.getContext().getParameters().add("tracing", "true"); + } } - protected Stream listTestCases() { - return Stream.of( - // new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.JETTY), // restore while taking care of #1444 - // new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.INTERNAL), // restore while taking care of #1444 - // new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY), // restore while taking care of #1444 - new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.INTERNAL) + protected abstract Application createApplication(); + + protected List listTestCases() { + return List.of( + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.INTERNAL), + new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.INTERNAL), + new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.JETTY), + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY) ); } @TestFactory - Stream dynamicTestsFromStream() { - return listTestCases() + Stream testsFactory() { + return listTestCases().stream() .map(testCase -> dynamicTest( testCase.getTestLabel(), - () -> { - runTest(testCase.httpServer, testCase.httpClient); - resetEngine(); - })); + () -> runTest(testCase.httpServer, testCase.httpClient))); } private void runTest(final HttpServer server, final HttpClient client) throws Exception { - // Engine.setLogLevel(Level.FINE); - Engine nre = Engine.register(false); - nre.getRegisteredServers().add(server.serverHelper); - nre.getRegisteredClients().add(client.clientHelper); - nre.registerDefaultAuthentications(); - nre.registerDefaultConverters(); + if (shouldDebug()) { + System.setProperty("org.eclipse.jetty.LEVEL", "TRACE"); + System.setProperty("sun.net.www.protocol.http.HttpURLConnection.LEVEL", "ALL"); + } + initEngine(server, client); start(); try { - doTestUri(getCallUri(port)); + doTest(port); } finally { stop(); + resetEngine(); + } + } + + private void start() throws Exception { + this.component = new Component(); + final Server server = createServer(this.component); + configureServer(server); + Application application = createApplication(); + + this.component.getDefaultHost().attach(application); + this.component.start(); + this.port = server.getEphemeralPort(); + } + + private void stop() throws Exception { + if ((this.component != null) && this.component.isStarted()) { + this.component.stop(); + } + this.component = null; + } + + private void initEngine(HttpServer server, HttpClient client) { + if (shouldDebug()) { + Engine.setLogLevel(Level.FINE); } + + Engine nre = Engine.register(false); + nre.getRegisteredServers().add(server.serverHelper); + nre.getRegisteredClients().add(client.clientHelper); + nre.registerDefaultAuthentications(); + nre.registerDefaultConverters(); + } + + private void resetEngine() { + // Restore a clean engine + org.restlet.engine.Engine.register(); } public enum HttpServer { @@ -99,7 +146,7 @@ public enum HttpServer { } public enum HttpClient { - INTERNAL(new org.restlet.engine.connector.HttpClientHelper(null)), JETTY(new org.restlet.ext.jetty.HttpClientHelper(null)); + INTERNAL(new HttpClientHelper(null)), JETTY(new org.restlet.ext.jetty.HttpClientHelper(null)); final ClientHelper clientHelper; @@ -108,26 +155,4 @@ public enum HttpClient { } } - private void start() throws Exception { - this.component = new Component(); - Server server = configureServer(this.component); - Application application = createApplication(); - - this.component.getDefaultHost().attach(application); - this.component.start(); - this.port = server.getEphemeralPort(); - } - - private void stop() throws Exception { - if ((this.component != null) && this.component.isStarted()) { - this.component.stop(); - } - this.component = null; - } - - private void resetEngine() { - // Restore a clean engine - org.restlet.engine.Engine.register(); - } - } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java index 618d868d49..5d37c49c73 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java @@ -19,6 +19,7 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -32,7 +33,9 @@ public class ChunkedEncodingPutTestCase extends BaseConnectorsTestCase { private static final int LOOP_NUMBER = 200; @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + for (int testIndex = 0; testIndex < LOOP_NUMBER; testIndex++) { sendPut(testIndex, uri, 10); } @@ -50,7 +53,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", PutTestResource.class); + router.attachDefault(PutTestResource.class); return router; } }; @@ -95,12 +98,12 @@ private void sendPut(int testIndex, final String uri, final int size) throws Exc System.out.println(response.getStatus()); } - assertNotNull(response.getEntity(), String.format("test #%d - size %d: response's entity is null", testIndex, size)); + assertNotNull(response.getEntity(), format("test #%d - size %d: response's entity is null", testIndex, size)); final String responseEntity = response.getEntity().getText(); - assertNotNull(responseEntity, String.format("test #%d - size %d: response's entity content is null", testIndex, size)); - assertEquals(size, responseEntity.length(), String.format("test #%d - size %d: length of response's entity is wrong", testIndex, size)); + assertNotNull(responseEntity, format("test #%d - size %d: response's entity content is null", testIndex, size)); + assertEquals(size, responseEntity.length(), format("test #%d - size %d: length of response's entity is wrong", testIndex, size)); final String expectedResponseEntity = createChunkedRepresentation(size).getText(); - assertEquals(expectedResponseEntity, responseEntity, String.format("test #%d - size %d: response's entity is wrong", testIndex, size)); + assertEquals(expectedResponseEntity, responseEntity, format("test #%d - size %d: response's entity is wrong", testIndex, size)); } finally { response.release(); client.stop(); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java index 28124fe493..78667d6005 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java @@ -21,6 +21,7 @@ import java.io.IOException; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.*; /** @@ -37,7 +38,7 @@ private static void assertXML(int testIndex, Representation entity) { try { String expected = ""; String text = entity.getText(); - assertEquals(expected, text, String.format("test #%d: xml representation is wrong", testIndex)); + assertEquals(expected, text, format("test #%d: xml representation is wrong", testIndex)); } catch (IOException ex) { fail(ex.getMessage()); } @@ -59,7 +60,9 @@ private static Representation createTestXml() { } @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + for (int testIndex = 0; testIndex < LOOP_NUMBER; testIndex++) { sendGet(testIndex, uri); sendPut(testIndex, uri); @@ -72,7 +75,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", PutTestResource.class); + router.attachDefault(PutTestResource.class); return router; } }; @@ -84,7 +87,7 @@ private void sendGet(int testIndex, String uri) throws Exception { final Response response = client.handle(request); try { - assertEquals(Status.SUCCESS_OK, response.getStatus(), String.format("test #%d: response's status is wrong", testIndex)); + assertEquals(Status.SUCCESS_OK, response.getStatus(), format("test #%d: response's status is wrong", testIndex)); assertXML(testIndex, response.getEntity()); } finally { response.release(); @@ -99,7 +102,7 @@ private void sendPut(int testIndex, String uri) throws Exception { try { assertChunkedHeader(response); - assertEquals(Status.SUCCESS_OK, response.getStatus(), String.format("test #%d: response's status is wrong", testIndex)); + assertEquals(Status.SUCCESS_OK, response.getStatus(), format("test #%d: response's status is wrong", testIndex)); assertXML(testIndex, response.getEntity()); } finally { response.release(); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java index 2a98395d6b..ec85ec8285 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java @@ -18,6 +18,7 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -29,8 +30,11 @@ public class GetChunkedTestCase extends BaseConnectorsTestCase { private static final String text = "" + "a".repeat(1000) + ""; + @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Client client = new Client(Protocol.HTTP); final Request request = new Request(Method.GET, uri); final Response response = client.handle(request); @@ -51,7 +55,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetChunkedTestResource.class); + router.attachDefault(GetChunkedTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java index 0f4a1b1be5..113fa028c1 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java @@ -22,6 +22,7 @@ import java.util.SortedMap; import java.util.TreeMap; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -32,12 +33,9 @@ public class GetQueryParamTestCase extends BaseConnectorsTestCase { @Override - protected String getCallUri(int port) { - return super.getCallUri(port) + "?q1=a&q2=b"; - } + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d?q1=a&q2=b", serverPort); - @Override - protected void doTestUri(String uri) throws Exception { final Client client = new Client(Protocol.HTTP); final Request request = new Request(Method.GET, uri); final Response response = client.handle(request); @@ -56,7 +54,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java index 6538e659f6..b4f6366aaf 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java @@ -9,13 +9,7 @@ package org.restlet.ext.jetty.connectors; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; +import org.restlet.*; import org.restlet.data.Method; import org.restlet.data.Protocol; import org.restlet.data.Status; @@ -23,6 +17,9 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * Test that a simple get works for all the connectors. * @@ -31,7 +28,9 @@ public class GetTestCase extends BaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Request request = new Request(Method.GET, uri); final Client client = new Client(Protocol.HTTP); final Response response = client.handle(request); @@ -54,7 +53,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java index b7b8d5544c..fbc273b266 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java @@ -9,6 +9,7 @@ package org.restlet.ext.jetty.connectors; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -30,7 +31,9 @@ public class PostPutTestCase extends BaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Client client = new Client(Protocol.HTTP); try { testCall(client, Method.POST, uri); @@ -50,7 +53,7 @@ public Restlet createInboundRoot() { public void handle(Request request, Response response) { Representation entity = request.getEntity(); if (entity != null) { - Form form = new Form(entity); + final Form form = new Form(entity); response.setEntity(form.getWebRepresentation()); } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java index b7bc4fb8e6..dbb72066ed 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java @@ -9,19 +9,7 @@ package org.restlet.ext.jetty.connectors; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.Enumeration; - -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; +import org.restlet.*; import org.restlet.data.MediaType; import org.restlet.data.Method; import org.restlet.data.Protocol; @@ -32,6 +20,15 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * Test that the client address is available for all the connectors * @@ -40,13 +37,16 @@ public class RemoteClientAddressTestCase extends BaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Client client = new Client(Protocol.HTTP); final Request request = new Request(Method.GET, uri); final Response response = client.handle(request); try { assertEquals(Status.SUCCESS_OK, response.getStatus()); + assertEquals("OK", response.getEntityAsText()); } finally { response.release(); client.stop(); @@ -59,7 +59,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", RemoteClientAddressResource.class); + router.attachDefault(RemoteClientAddressResource.class); return router; } }; @@ -74,15 +74,15 @@ public RemoteClientAddressResource() { @Override public Representation get(Variant variant) { boolean localAddress = false; + try { - Enumeration n = NetworkInterface - .getNetworkInterfaces(); - for (; n.hasMoreElements();) { - NetworkInterface e = n.nextElement(); - Enumeration a = e.getInetAddresses(); - for (; a.hasMoreElements();) { - InetAddress addr = a.nextElement(); - if (addr.getHostAddress().equals( + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + Enumeration inetAddresses = networkInterface.getInetAddresses(); + while (inetAddresses.hasMoreElements()) { + final InetAddress inetAddress = inetAddresses.nextElement(); + if (inetAddress.getHostAddress().equals( getRequest().getClientInfo().getAddress())) { localAddress = true; } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java new file mode 100644 index 0000000000..5319fb13ab --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java @@ -0,0 +1,141 @@ +/** + * Copyright 2005-2024 Qlik + *

+ * The contents of this file is subject to the terms of the Apache 2.0 open + * source license available at http://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ + +package org.restlet.ext.jetty.connectors; + +import org.restlet.*; +import org.restlet.data.Method; +import org.restlet.data.Parameter; +import org.restlet.data.Protocol; +import org.restlet.data.Status; +import org.restlet.engine.connector.HttpClientHelper; +import org.restlet.util.Series; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This tests the ability of the server to accept a fixed number of incoming connections. + */ +public class ServerMaxConnectionsTestCase extends BaseConnectorsTestCase { + private static final Logger LOGGER = Logger.getLogger(ServerMaxConnectionsTestCase.class.getCanonicalName()); + private final static int CONNECTIONS_NUMBER = 1; + private final static int CONCURRENT_REQUESTS = 2; + private final static Duration SERVER_RESOURCE_FREEZE_DURATION = Duration.ofSeconds(1); + + @Override + protected void configureServer(final Server server) { + super.configureServer(server); + + final Series parameters = server.getContext().getParameters(); + parameters.add("server.maxConnections", Integer.toString(CONNECTIONS_NUMBER)); + parameters.add("connector.acceptors", Integer.toString(CONCURRENT_REQUESTS)); // server can accept all requests + } + + @Override + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + + final CountDownLatch countDownLatch = new CountDownLatch(CONCURRENT_REQUESTS); + + final List testResults = new ArrayList<>(); + + final Executor executor = Executors.newFixedThreadPool(CONCURRENT_REQUESTS); + final Runnable runnable = () -> { + testResults.add(sendGet(uri).isSuccess()); + countDownLatch.countDown(); + log("client countDownLatch.countDown done " + Thread.currentThread().getName()); + }; + + for (int i = 0; i < CONCURRENT_REQUESTS; i++) { + executor.execute(runnable); + } + + log("test before countDownLatch.await()"); + countDownLatch.await(); + log("test after countDownLatch.await()"); + + assertEquals(CONCURRENT_REQUESTS, testResults.size()); + assertTrue(testResults.contains(true)); // One has succeeded + assertTrue(testResults.contains(false)); // The other has failed + } + + private static void log(final String message) { + LOGGER.fine(message + " " + Thread.currentThread()); + } + + @Override + protected List listTestCases() { + return List.of( + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY), + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.INTERNAL) + ); + } + + private Status sendGet(final String uri) { + final Status result; + + log("client send get " + Thread.currentThread().getName()); + try { + final Request request = new Request(Method.GET, uri + "/" + Thread.currentThread().getName()); + final Client client = new Client(new Context(), Protocol.HTTP); + + if (client.getContext().getAttributes().get("org.restlet.engine.helper") instanceof HttpClientHelper) { + // Specific to the internal client + // When Jetty refuses the extra connection, this does not block the underlying native HttpClient... + // Let's set a timeout higher than the wait time imposed by the server (otherwise all requests will fail) + String readTimeoutInMs = Long.toString(SERVER_RESOURCE_FREEZE_DURATION.plus(Duration.ofSeconds(1)).toMillis()); + client.getContext().getParameters().add("readTimeout", readTimeoutInMs); + } + + final Response response = client.handle(request); + log("client get sent " + Thread.currentThread().getName()); + result = response.getStatus(); + log("client status " + result + " " + Thread.currentThread().getName()); + response.release(); + client.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + log("client done " + Thread.currentThread().getName()); + return result; + } + + @Override + protected Application createApplication() { + return new Application() { + @Override + public Restlet createInboundRoot() { + return new Restlet() { + @Override + public void handle(Request request, Response response) { + try { + log("server resource wait"); + Thread.sleep(SERVER_RESOURCE_FREEZE_DURATION.toMillis()); + log("server resource wait ended"); + response.setStatus(Status.SUCCESS_NO_CONTENT); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + } + }; + } + +} diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java index bc23de2422..d06037f6f3 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java @@ -10,11 +10,8 @@ package org.restlet.ext.jetty.connectors; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.restlet.*; -import org.restlet.data.Method; import org.restlet.data.Parameter; import org.restlet.data.Protocol; import org.restlet.engine.io.IoUtils; @@ -22,7 +19,7 @@ import java.io.*; import java.nio.file.Files; -import java.util.stream.Stream; +import java.util.List; /** * Base test case that will call an abstract method for several client/server @@ -35,35 +32,17 @@ @SuppressWarnings("unused") public abstract class SslBaseConnectorsTestCase extends BaseConnectorsTestCase { + protected static final String KEYSTORE_FILE_NAME = "dummy.p12"; + protected static final String KEYSTORE_PASSWORD = "testtest"; + protected static final String KEYSTORE_TYPE = "PKCS12"; protected static File testKeystoreFile; - protected void configureSslClientParameters(Context context) { - Series parameters = context.getParameters(); - parameters.add("truststorePath", testKeystoreFile.getPath()); - parameters.add("truststorePassword", "testtest"); - } - - protected void configureSslServerParameters(Context context) { - Series parameters = context.getParameters(); - parameters.add("keystorePath", testKeystoreFile.getPath()); - parameters.add("keystorePassword", "testtest"); - parameters.add("keyPassword", "testtest"); - parameters.add("truststorePath", testKeystoreFile.getPath()); - parameters.add("truststorePassword", "testtest"); - // parameters.add("tracing", "true"); - } - - @Override - protected String getCallUri(final int port) { - return "https://localhost:" + port + "/test"; - } - @BeforeAll public static void globalSetUp() throws IOException { - testKeystoreFile = Files.createTempFile("sslBaseConnectorsTest", "dummy.jks").toFile(); + testKeystoreFile = Files.createTempFile("sslBaseConnectorsTest", KEYSTORE_FILE_NAME).toFile(); testKeystoreFile.delete(); - InputStream resourceAsStream = SslBaseConnectorsTestCase.class.getResourceAsStream("dummy.jks"); + InputStream resourceAsStream = SslBaseConnectorsTestCase.class.getResourceAsStream(KEYSTORE_FILE_NAME); if (resourceAsStream != null) { OutputStream outputStream = new FileOutputStream(testKeystoreFile); IoUtils.copy(resourceAsStream, outputStream); @@ -76,21 +55,53 @@ public static void globalSetUp() throws IOException { } @Override - protected Stream listTestCases() { - return Stream.of( + protected List listTestCases() { + return List.of( new ConnectorTestCase(HttpServer.INTERNAL_HTTPS, HttpClient.JETTY), - // new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.INTERNAL), // restore while taking care of #1444 - // new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.JETTY), // restore while taking care of #1444 + new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.INTERNAL), + new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.JETTY), new ConnectorTestCase(HttpServer.INTERNAL_HTTPS, HttpClient.INTERNAL) ); } @Override - protected Server configureServer(final Component component) { - final Server server = component.getServers().add(Protocol.HTTPS, 0); - configureSslServerParameters(server.getContext()); - // server.getContext().getParameters().add("tracing", "true"); - return server; + protected Server createServer(Component component) { + return component.getServers().add(Protocol.HTTPS, 0); + } + + @Override + protected void configureServer(final Server server) { + super.configureServer(server); + configureSslServerParameters(server); + } + + protected void configureSslClientParameters(final Client client) { + Series parameters = client.getContext().getParameters(); + + parameters.add("truststorePath", testKeystoreFile.getPath()); + parameters.add("truststorePassword", KEYSTORE_PASSWORD); + parameters.add("trustStoreType", KEYSTORE_TYPE); + if (shouldDebug()) { + parameters.add("tracing", "true"); + } + } + + protected void configureSslServerParameters(final Server server) { + Series parameters = server.getContext().getParameters(); + + parameters.add("keystorePath", testKeystoreFile.getPath()); + parameters.add("keystorePassword", KEYSTORE_PASSWORD); + parameters.add("keyStoreType", KEYSTORE_TYPE); + parameters.add("keyPassword", KEYSTORE_PASSWORD); + + parameters.add("truststorePath", testKeystoreFile.getPath()); + parameters.add("truststorePassword", KEYSTORE_PASSWORD); + parameters.add("trustStoreType", KEYSTORE_TYPE); + + if (shouldDebug()) { + System.setProperty("javax.net.debug", "ssl:handshake"); + parameters.add("tracing", "true"); + } } @AfterAll diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java index 244f5db8ff..1b879632bf 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java @@ -9,23 +9,17 @@ package org.restlet.ext.jetty.connectors; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; -import org.restlet.data.MediaType; -import org.restlet.data.Method; -import org.restlet.data.Protocol; -import org.restlet.data.Status; +import org.restlet.*; +import org.restlet.data.*; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.representation.Variant; import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import org.restlet.util.Series; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test that a simple get using SSL works for all the connectors. @@ -36,20 +30,33 @@ public class SslClientContextGetTestCase extends SslBaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { - final Request request = new Request(Method.GET, uri); - final Client client = new Client(Protocol.HTTPS); - if (client.getContext() == null) { - client.setContext(new Context()); - } - configureSslServerParameters(client.getContext()); - final Response response = client.handle(request); + protected void doTest(final int serverPort) throws Exception { + final String uri = format("https://localhost:%d", serverPort); + + final Response response = sendGet(uri); assertEquals(Status.SUCCESS_OK, response.getStatus(), response.getStatus().getDescription()); assertEquals("Hello world", response.getEntity().getText()); + } + + private Response sendGet(final String uri) { + final Client client = new Client(Protocol.HTTPS); + client.setContext(new Context()); + configureSslClientParameters(client); + + final Request request = new Request(Method.GET, uri); + return client.handle(request); + } + + @Override + protected void configureSslClientParameters(final Client client) { + super.configureSslClientParameters(client); - Thread.sleep(200); - client.stop(); + Series parameters = client.getContext().getParameters(); + parameters.add("keystorePath", testKeystoreFile.getPath()); + parameters.add("keystorePassword", KEYSTORE_PASSWORD); + parameters.add("keyPassword", KEYSTORE_PASSWORD); + parameters.add("keyStoreType", KEYSTORE_TYPE); } @Override @@ -58,7 +65,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java index e9140a9037..65935aef4a 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java @@ -9,22 +9,16 @@ package org.restlet.ext.jetty.connectors; -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; -import org.restlet.data.MediaType; -import org.restlet.data.Method; -import org.restlet.data.Protocol; -import org.restlet.data.Status; +import org.restlet.*; +import org.restlet.data.*; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.representation.Variant; import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import org.restlet.util.Series; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -35,32 +29,21 @@ */ public class SslGetTestCase extends SslBaseConnectorsTestCase { - public static class GetTestResource extends ServerResource { - - public GetTestResource() { - - getVariants().add(new Variant(MediaType.TEXT_PLAIN)); - } + @Override + protected void doTest(final int serverPort) throws Exception { + final Response response = sendGet(format("https://localhost:%d", serverPort)); - @Override - public Representation get(Variant variant) { - return new StringRepresentation("Hello world", MediaType.TEXT_PLAIN); - } + assertEquals(Status.SUCCESS_OK, response.getStatus(), response.getStatus().getDescription()); + assertEquals("Hello world", response.getEntity().getText()); } - @Override - protected void doTestUri(String uri) throws Exception { - final Request request = new Request(Method.GET, uri); + private Response sendGet(final String uri) { final Client client = new Client(Protocol.HTTPS); client.setContext(new Context()); - configureSslClientParameters(client.getContext()); - final Response r = client.handle(request); - - assertEquals(Status.SUCCESS_OK, r.getStatus(), r.getStatus().getDescription()); - assertEquals("Hello world", r.getEntity().getText()); + configureSslClientParameters(client); - Thread.sleep(200); - client.stop(); + final Request request = new Request(Method.GET, uri); + return client.handle(request); } @Override @@ -69,9 +52,22 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; } + + public static class GetTestResource extends ServerResource { + + public GetTestResource() { + getVariants().add(new Variant(MediaType.TEXT_PLAIN)); + } + + @Override + public Representation get(Variant variant) { + return new StringRepresentation("Hello world", MediaType.TEXT_PLAIN); + } + } + } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/resources/org/restlet/ext/jetty/connectors/dummy.p12 b/org.restlet.java/org.restlet.ext.jetty/src/test/resources/org/restlet/ext/jetty/connectors/dummy.p12 new file mode 100644 index 0000000000..94519e717d Binary files /dev/null and b/org.restlet.java/org.restlet.ext.jetty/src/test/resources/org/restlet/ext/jetty/connectors/dummy.p12 differ diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java index 61a4fefd3f..2e0c78e18f 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java @@ -85,13 +85,7 @@ public ClientAdapter getAdapter() throws Exception { * @return The connection timeout. */ public int getSocketConnectTimeoutMs() { - int result = 0; - - if (getHelpedParameters().getNames().contains("socketConnectTimeoutMs")) { - result = Integer.parseInt(getHelpedParameters().getFirstValue("socketConnectTimeoutMs", "15000")); - } - - return result; + return Integer.parseInt(getHelpedParameters().getFirstValue("socketConnectTimeoutMs", "15000")); } @Override diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java index bae71582a9..2d6174190c 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java @@ -143,7 +143,7 @@ public void commit(HttpResponse response) { response.getHttpCall().sendResponse(response); } catch (Throwable t) { if (response.getHttpCall().isConnectionBroken(t)) { - // output a single log line for this common case to avoid filling servers logs + // output a single log line for this common case to avoid filling server logs getLogger().log(Level.INFO, "The connection was broken. It was probably closed by the client. Reason: " + t.getMessage()); } else { diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/ClientHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/ClientHelper.java index 69e9ffa8d9..05562a2374 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/ClientHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/ClientHelper.java @@ -12,7 +12,7 @@ import org.restlet.Client; /** - * Client connector helper. Base client helper based on NIO non blocking + * Client connector helper. Base client helper based on NIO non-blocking * sockets. Here is the list of parameters that are supported. They should be * set in the Client's context before it is started: * diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java index 9a1e3d8c84..7d450cb4cb 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java @@ -89,9 +89,9 @@ * the {@link #getHostnameVerifier()} method for details. *

* Note that by default, the {@link HttpURLConnection} class as implemented by - * Sun will retry a request if an IO exception is caught, for example due to a + * Sun will retry a request if an IO exception is caught, for example, due to a * connection reset by the server. This can be annoying, especially because the - * HTTP semantics of non idempotent methods like POST can be broken, but also + * HTTP semantics of non-idempotent methods like POST can be broken, but also * because the new request won't include an entity. There is one way to disable * this behavior for POST requests only by setting the system property * "sun.net.http.retryPost" to "false". @@ -159,7 +159,7 @@ public HostnameVerifier getHostnameVerifier() { /** * Returns the read timeout value. A timeout of zero is interpreted as an - * infinite timeout. Defaults to 60000. + * infinite timeout. Default to 60000. * * @return The read timeout value. */ @@ -178,9 +178,9 @@ public boolean isAllowUserInteraction() { } /** - * Indicates if the protocol will automatically follow redirects. + * Indicates if the protocol automatically follows redirects. * - * @return True if the protocol will automatically follow redirects. + * @return True if the protocol automatically follows redirects. */ public boolean isFollowRedirects() { return Boolean.parseBoolean(getHelpedParameters().getFirstValue("followRedirects", "false")); diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java index 5d35ad8a3b..475414663a 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java @@ -48,7 +48,7 @@ public class HttpUrlConnectionCall extends ClientCall { * @param helper The parent HTTP client helper. * @param method The method name. * @param requestUri The request URI. - * @param hasEntity Indicates if the call will have an entity to send to the + * @param hasEntity Indicates if the call has an entity to send to the * server. * @throws IOException */ @@ -60,8 +60,7 @@ public HttpUrlConnectionCall(HttpClientHelper helper, String method, String requ URL url = new URL(requestUri); this.connection = (HttpURLConnection) url.openConnection(); - // These properties can only be used with Java 1.5 and upper - // releases + // These properties can only be used with Java 1.5 and upper releases int majorVersionNumber = SystemUtils.getJavaMajorVersion(); int minorVersionNumber = SystemUtils.getJavaMinorVersion(); if ((majorVersionNumber > 1) || ((majorVersionNumber == 1) && (minorVersionNumber >= 5))) { @@ -201,7 +200,7 @@ public Series

getResponseHeaders() { headerName = getConnection().getHeaderFieldKey(i); headerValue = getConnection().getHeaderField(i); } catch (java.util.NoSuchElementException e) { - // Some implementations especially the one for Google App + // Some implementations, especially the one for Google App // Engine throws a NoSuchElementException though this is not // stated by the contract of the abstract class // HttpUrlConnection. @@ -212,7 +211,7 @@ public Series
getResponseHeaders() { } else { // As stated by the HttpUrlConnection javadocs, some // implementations may treat the 0th header field as - // special, i.e. as the status line returned by the HTTP + // special, i.e., as the status line returned by the HTTP // server. loop = (i == 0); } @@ -249,8 +248,8 @@ public int getStatusCode() throws IOException { } /** - * Sends the request to the client. Commits the request line, headers and - * optional entity and send them over the network. + * Sends the request to the client. + * Commits the request line, headers, and optional entity and send them over the network. * * @param request The high-level request. * @return The result status. diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java index d56b2fa9d4..0b80dce2cd 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java @@ -268,10 +268,13 @@ public javax.net.ssl.SSLContext createSslContext() throws Exception { if ((this.keyStorePath != null) || (this.keyStoreProvider != null) || (this.keyStoreType != null)) { // Loads the key store. - KeyStore keyStore = (this.keyStoreProvider != null) - ? KeyStore.getInstance((this.keyStoreType != null) ? this.keyStoreType : KeyStore.getDefaultType(), - this.keyStoreProvider) - : KeyStore.getInstance((this.keyStoreType != null) ? this.keyStoreType : KeyStore.getDefaultType()); + final String nonNullKeyStoreType = (this.keyStoreType != null) + ? this.keyStoreType + : KeyStore.getDefaultType(); + final KeyStore keyStore = (this.keyStoreProvider != null) + ? KeyStore.getInstance(nonNullKeyStoreType, this.keyStoreProvider) + : KeyStore.getInstance(nonNullKeyStoreType); + FileInputStream keyStoreInputStream = null; try { @@ -294,12 +297,11 @@ public javax.net.ssl.SSLContext createSslContext() throws Exception { if ((this.trustStorePath != null) || (this.trustStoreProvider != null) || (this.trustStoreType != null)) { // Loads the trust store. + String nonNullTrustStoreType = (this.trustStoreType != null) ? this.trustStoreType : KeyStore.getDefaultType(); KeyStore trustStore = (this.trustStoreProvider != null) - ? KeyStore.getInstance( - (this.trustStoreType != null) ? this.trustStoreType : KeyStore.getDefaultType(), - this.trustStoreProvider) - : KeyStore.getInstance( - (this.trustStoreType != null) ? this.trustStoreType : KeyStore.getDefaultType()); + ? KeyStore.getInstance(nonNullTrustStoreType, this.trustStoreProvider) + : KeyStore.getInstance(nonNullTrustStoreType); + FileInputStream trustStoreInputStream = null; try { @@ -329,7 +331,7 @@ public javax.net.ssl.SSLContext createSslContext() throws Exception { sslContext.init(kmf != null ? kmf.getKeyManagers() : null, tmf != null ? tmf.getTrustManagers() : null, sr); // Wraps the SSL context to be able to set cipher suites and other - // properties after SSL engine creation for example + // properties after SSL engine creation, for example result = createWrapper(sslContext); return result; } diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java index 46233894c8..c799514448 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java @@ -92,7 +92,7 @@ public static Integer extractKeySize(String sslCipherSuite) { } /** - * Returns the SSL context factory. It first look for a "sslContextFactory" + * Returns the SSL context factory. It first looks for a "sslContextFactory" * attribute (instance), then for a "sslContextFactory" parameter (class name to * instantiate). * @@ -102,7 +102,8 @@ public static Integer extractKeySize(String sslCipherSuite) { */ public static SslContextFactory getSslContextFactory(RestletHelper helper) { - SslContextFactory result = (SslContextFactory) ((helper.getContext() == null) ? null + SslContextFactory result = (SslContextFactory) ((helper.getContext() == null) + ? null : helper.getContext().getAttributes().get("sslContextFactory")); if (result == null) { diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java index 1c99c8c0e1..df5339143f 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java @@ -44,7 +44,7 @@ public CertificateAuthenticator(Context context) { /** * Extracts the Principal of the subject to use from a chain of certificate. By - * default, this is the X500Principal of the subject subject of the first + * default, this is the X500Principal of the subject of the first * certificate in the chain. * * @see X509Certificate @@ -55,7 +55,7 @@ public CertificateAuthenticator(Context context) { protected List getPrincipals(List certificateChain) { ArrayList principals = null; - if ((certificateChain != null) && (certificateChain.size() > 0)) { + if ((certificateChain != null) && (!certificateChain.isEmpty())) { Certificate userCert = certificateChain.get(0); if (userCert instanceof X509Certificate) { diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java index 3f337826b5..62b7635b59 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java @@ -205,7 +205,7 @@ public T getFirst(String name, boolean ignoreCase) { /** * Returns the value of the first parameter found with the given name. * - * @param name The parameter name (case sensitive). + * @param name The parameter name (case-sensitive). * @return The value of the first parameter found with the given name. */ public String getFirstValue(String name) { @@ -216,7 +216,7 @@ public String getFirstValue(String name) { * Returns the value of the first parameter found with the given name. * * @param name The parameter name. - * @param ignoreCase Indicates if the name comparison is case sensitive. + * @param ignoreCase Indicates if the name comparison is case-sensitive. * @return The value of the first parameter found with the given name. */ public String getFirstValue(String name, boolean ignoreCase) { diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java index 4222d30e2c..57aa444395 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java @@ -15,8 +15,8 @@ import org.restlet.data.Method; import org.restlet.data.Protocol; import org.restlet.representation.StringRepresentation; -import org.restlet.routing.Redirector; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertNotNull; /** @@ -61,14 +61,11 @@ public void testRedirect() throws Exception { @Override public void handle(Request request, Response response) { // Print the requested URI path - final String message = "Resource URI: " - + request.getResourceRef() + '\n' + "Base URI: " - + request.getResourceRef().getBaseRef() + '\n' - + "Remaining part: " - + request.getResourceRef().getRemainingPart() + '\n' + final String message = "Resource URI: " + request.getResourceRef() + '\n' + + "Base URI: " + request.getResourceRef().getBaseRef() + '\n' + + "Remaining part: " + request.getResourceRef().getRemainingPart() + '\n' + "Method name: " + request.getMethod() + '\n'; - response.setEntity(new StringRepresentation(message, - MediaType.TEXT_PLAIN)); + response.setEntity(new StringRepresentation(message, MediaType.TEXT_PLAIN)); } }; @@ -87,12 +84,11 @@ public void handle(Request request, Response response) { // Tests final Context context = clientComponent.getContext(); - String uri = "http://localhost:" + TEST_PORT + "/?foo=bar"; + String uri = format("http://localhost:%d/?foo=bar", TEST_PORT); testCall(context, Method.GET, uri); testCall(context, Method.DELETE, uri); - uri = "http://localhost:" + TEST_PORT - + "/abcd/efgh/ijkl?foo=bar&foo=beer"; + uri = format("http://localhost:%d/abcd/efgh/ijkl?foo=bar&foo=beer", TEST_PORT); testCall(context, Method.GET, uri); testCall(context, Method.DELETE, uri);