shutdown = new CompletableFuture<>()
+ {
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning)
+ {
+ boolean canceled = super.cancel(mayInterruptIfRunning);
+ if (canceled && mayInterruptIfRunning)
+ {
+ Thread thread = stopThreadReference.get();
+ if (thread != null)
+ thread.interrupt();
+ }
+
+ return canceled;
+ }
+ };
+
+ Thread stopThread = new Thread(() ->
+ {
+ try
+ {
+ runnable.run();
+ shutdown.complete(null);
+ }
+ catch (Throwable t)
+ {
+ shutdown.completeExceptionally(t);
+ }
+ });
+ stopThread.setDaemon(true);
+ stopThreadReference.set(stopThread);
+ stopThread.start();
+ return shutdown;
+ }
+
+ @FunctionalInterface
+ interface ThrowingRunnable
+ {
+ void run() throws Exception;
+ }
}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java
index 8cd13eff164..ee919a2a6b5 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java
@@ -27,8 +27,8 @@ import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* The {@link KeyStoreScanner} is used to monitor the KeyStore file used by the {@link SslContextFactory}.
@@ -38,7 +38,7 @@ import org.eclipse.jetty.util.log.Logger;
*/
public class KeyStoreScanner extends ContainerLifeCycle implements Scanner.DiscreteListener
{
- private static final Logger LOG = Log.getLogger(KeyStoreScanner.class);
+ private static final Logger LOG = LoggerFactory.getLogger(KeyStoreScanner.class);
private final SslContextFactory sslContextFactory;
private final File keystoreFile;
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/PoolTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/PoolTest.java
new file mode 100644
index 00000000000..5b811728867
--- /dev/null
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/PoolTest.java
@@ -0,0 +1,442 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under
+// the terms of the Eclipse Public License 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0
+//
+// This Source Code may also be made available under the following
+// Secondary Licenses when the conditions for such availability set
+// forth in the Eclipse Public License, v. 2.0 are satisfied:
+// the Apache License v2.0 which is available at
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import java.io.Closeable;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.jupiter.api.Test;
+
+import static java.util.stream.Collectors.toList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class PoolTest
+{
+
+ @Test
+ public void testAcquireRelease()
+ {
+ Pool pool = new Pool<>(1,0);
+ pool.reserve(-1).enable("aaa");
+
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(true));
+ Pool.Entry e1 = pool.acquire();
+ assertThat(e1.getPooled(), equalTo("aaa"));
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(false));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.release(e1), is(true));
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(true));
+ assertThrows(IllegalStateException.class, () -> pool.release(e1));
+ Pool.Entry e2 = pool.acquire();
+ assertThat(e2.getPooled(), equalTo("aaa"));
+ assertThat(pool.release(e2), is(true));
+ assertThrows(NullPointerException.class, () -> pool.release(null));
+ }
+
+ @Test
+ public void testRemoveBeforeRelease()
+ {
+ Pool pool = new Pool<>(1,0);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.release(e1), is(false));
+ }
+
+ @Test
+ public void testCloseBeforeRelease()
+ {
+ Pool pool = new Pool<>(1,0);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(pool.size(), is(1));
+ pool.close();
+ assertThat(pool.size(), is(0));
+ assertThat(pool.release(e1), is(false));
+ }
+
+ @Test
+ public void testMaxPoolSize()
+ {
+ Pool pool = new Pool<>(1, 0);
+ assertThat(pool.size(), is(0));
+ assertThat(pool.reserve(-1), notNullValue());
+ assertThat(pool.size(), is(1));
+ assertThat(pool.reserve(-1), nullValue());
+ assertThat(pool.size(), is(1));
+ }
+
+ @Test
+ public void testReserve()
+ {
+ Pool pool = new Pool<>(2, 0);
+ Pool.Entry entry = pool.reserve(-1);
+ assertThat(pool.size(), is(1));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(entry.isClosed(), is(true));
+
+ assertThrows(NullPointerException.class, () -> entry.enable(null));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(entry.isClosed(), is(true));
+
+ entry.enable("aaa");
+ assertThat(entry.isClosed(), is(false));
+ assertThat(pool.acquire().getPooled(), notNullValue());
+
+ assertThrows(IllegalStateException.class, () -> entry.enable("bbb"));
+
+ Pool.Entry e2 = pool.reserve(-1);
+ assertThat(pool.size(), is(2));
+ assertThat(pool.remove(e2), is(true));
+ assertThat(pool.size(), is(1));
+
+ pool.reserve(-1);
+ assertThat(pool.size(), is(2));
+ pool.close();
+ assertThat(pool.size(), is(0));
+ assertThat(pool.reserve(-1), nullValue());
+ assertThat(entry.isClosed(), is(true));
+ }
+
+ @Test
+ public void testReserveMaxPending()
+ {
+ Pool pool = new Pool<>(2, 0);
+ assertThat(pool.reserve(0), nullValue());
+ assertThat(pool.reserve(1), notNullValue());
+ assertThat(pool.reserve(1), nullValue());
+ assertThat(pool.reserve(2), notNullValue());
+ assertThat(pool.reserve(2), nullValue());
+ assertThat(pool.reserve(3), nullValue());
+ assertThat(pool.reserve(-1), nullValue());
+ }
+
+ @Test
+ public void testReserveNegativeMaxPending()
+ {
+ Pool pool = new Pool<>(2, 0);
+ assertThat(pool.reserve(-1), notNullValue());
+ assertThat(pool.reserve(-1), notNullValue());
+ assertThat(pool.reserve(-1), nullValue());
+ }
+
+ @Test
+ public void testClose()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.reserve(-1).enable("aaa");
+ assertThat(pool.isClosed(), is(false));
+ pool.close();
+ pool.close();
+
+ assertThat(pool.isClosed(), is(true));
+ assertThat(pool.size(), is(0));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.reserve(-1), nullValue());
+ }
+
+ @Test
+ public void testClosingCloseable()
+ {
+ AtomicBoolean closed = new AtomicBoolean();
+ Pool pool = new Pool<>(1,0);
+ Closeable pooled = () -> closed.set(true);
+ pool.reserve(-1).enable(pooled);
+ assertThat(closed.get(), is(false));
+ pool.close();
+ assertThat(closed.get(), is(true));
+ }
+
+ @Test
+ public void testRemove()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.release(e1), is(false));
+ assertThat(pool.acquire(), nullValue());
+ assertThrows(NullPointerException.class, () -> pool.remove(null));
+ }
+
+ @Test
+ public void testValuesSize()
+ {
+ Pool pool = new Pool<>(2, 0);
+
+ assertThat(pool.size(), is(0));
+ assertThat(pool.values().isEmpty(), is(true));
+ pool.reserve(-1).enable("aaa");
+ pool.reserve(-1).enable("bbb");
+ assertThat(pool.values().stream().map(Pool.Entry::getPooled).collect(toList()), equalTo(Arrays.asList("aaa", "bbb")));
+ assertThat(pool.size(), is(2));
+ }
+
+ @Test
+ public void testValuesContainsAcquiredEntries()
+ {
+ Pool pool = new Pool<>(2, 0);
+
+ pool.reserve(-1).enable("aaa");
+ pool.reserve(-1).enable("bbb");
+ assertThat(pool.acquire(), notNullValue());
+ assertThat(pool.acquire(), notNullValue());
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.values().isEmpty(), is(false));
+ }
+
+ @Test
+ public void testAcquireAt()
+ {
+ Pool pool = new Pool<>(2, 0);
+
+ pool.reserve(-1).enable("aaa");
+ pool.reserve(-1).enable("bbb");
+
+ assertThat(pool.acquireAt(2), nullValue());
+ assertThat(pool.acquireAt(0), notNullValue());
+ assertThat(pool.acquireAt(0), nullValue());
+ assertThat(pool.acquireAt(1), notNullValue());
+ assertThat(pool.acquireAt(1), nullValue());
+ }
+
+ @Test
+ public void testMaxUsageCount()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxUsageCount(3);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ e1 = pool.acquire();
+ assertThat(pool.release(e1), is(false));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.size(), is(0));
+ Pool.Entry e1Copy = e1;
+ assertThat(pool.release(e1Copy), is(false));
+ }
+
+ @Test
+ public void testMaxMultiplex()
+ {
+ Pool pool = new Pool<>(2, 0);
+ pool.setMaxMultiplex(3);
+ pool.reserve(-1).enable("aaa");
+ pool.reserve(-1).enable("bbb");
+
+ Pool.Entry e1 = pool.acquire();
+ Pool.Entry e2 = pool.acquire();
+ Pool.Entry e3 = pool.acquire();
+ Pool.Entry e4 = pool.acquire();
+ assertThat(e1.getPooled(), equalTo("aaa"));
+ assertThat(e1, sameInstance(e2));
+ assertThat(e1, sameInstance(e3));
+ assertThat(e4.getPooled(), equalTo("bbb"));
+ assertThat(pool.release(e1), is(true));
+ Pool.Entry e5 = pool.acquire();
+ assertThat(e2, sameInstance(e5));
+ Pool.Entry e6 = pool.acquire();
+ assertThat(e4, sameInstance(e6));
+ }
+
+ @Test
+ public void testRemoveMultiplexed()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxMultiplex(2);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ Pool.Entry e2 = pool.acquire();
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(false));
+
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(false));
+ assertThat(pool.values().stream().findFirst().get().isClosed(), is(true));
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.size(), is(0));
+
+ assertThat(pool.remove(e1), is(false));
+
+ assertThat(pool.release(e1), is(false));
+
+ assertThat(pool.remove(e1), is(false));
+ }
+
+ @Test
+ public void testMultiplexRemoveThenAcquireThenReleaseRemove()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxMultiplex(2);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ Pool.Entry e2 = pool.acquire();
+
+ assertThat(pool.remove(e1), is(false));
+ assertThat(e1.isClosed(), is(true));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(pool.release(e2), is(false));
+ assertThat(pool.remove(e2), is(true));
+ }
+
+ @Test
+ public void testNonMultiplexRemoveAfterAcquire()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxMultiplex(2);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(pool.remove(e1), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @Test
+ public void testMultiplexRemoveAfterAcquire()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxMultiplex(2);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ Pool.Entry e2 = pool.acquire();
+
+ assertThat(pool.remove(e1), is(false));
+ assertThat(pool.remove(e2), is(true));
+ assertThat(pool.size(), is(0));
+
+ assertThat(pool.release(e1), is(false));
+ assertThat(pool.size(), is(0));
+
+ Pool.Entry e3 = pool.acquire();
+ assertThat(e3, nullValue());
+
+ assertThat(pool.release(e2), is(false));
+ assertThat(pool.size(), is(0));
+ }
+
+ @Test
+ public void testReleaseThenRemoveNonEnabledEntry()
+ {
+ Pool pool = new Pool<>(1, 0);
+ Pool.Entry e = pool.reserve(-1);
+ assertThat(pool.size(), is(1));
+ assertThat(pool.release(e), is(false));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @Test
+ public void testRemoveNonEnabledEntry()
+ {
+ Pool pool = new Pool<>(1, 0);
+ Pool.Entry e = pool.reserve(-1);
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @Test
+ public void testMultiplexMaxUsageReachedAcquireThenRemove()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxMultiplex(2);
+ pool.setMaxUsageCount(3);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e0 = pool.acquire();
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ Pool.Entry e2 = pool.acquire();
+ assertThat(pool.release(e2), is(true));
+ assertThat(pool.acquire(), nullValue());
+
+ assertThat(pool.remove(e0), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @Test
+ public void testMultiplexMaxUsageReachedAcquireThenReleaseThenRemove()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxMultiplex(2);
+ pool.setMaxUsageCount(3);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e0 = pool.acquire();
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(pool.release(e1), is(true));
+ Pool.Entry e2 = pool.acquire();
+ assertThat(pool.release(e2), is(true));
+ assertThat(pool.acquire(), nullValue());
+
+ assertThat(pool.release(e0), is(false));
+ assertThat(pool.values().stream().findFirst().get().isIdle(), is(true));
+ assertThat(pool.values().stream().findFirst().get().isClosed(), is(false));
+ assertThat(pool.size(), is(1));
+ assertThat(pool.remove(e0), is(true));
+ assertThat(pool.size(), is(0));
+ }
+
+ @Test
+ public void testUsageCountAfterReachingMaxMultiplexLimit()
+ {
+ Pool pool = new Pool<>(1, 0);
+ pool.setMaxMultiplex(2);
+ pool.setMaxUsageCount(10);
+ pool.reserve(-1).enable("aaa");
+
+ Pool.Entry e1 = pool.acquire();
+ assertThat(e1.getUsageCount(), is(1));
+ Pool.Entry e2 = pool.acquire();
+ assertThat(e1.getUsageCount(), is(2));
+ assertThat(pool.acquire(), nullValue());
+ assertThat(e1.getUsageCount(), is(2));
+ }
+
+ @Test
+ public void testConfigLimits()
+ {
+ assertThrows(IllegalArgumentException.class, () -> new Pool(1, 0).setMaxMultiplex(0));
+ assertThrows(IllegalArgumentException.class, () -> new Pool(1, 0).setMaxMultiplex(-1));
+ assertThrows(IllegalArgumentException.class, () -> new Pool(1, 0).setMaxUsageCount(0));
+ }
+}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
index 71f50eabf9d..5afbd2a84b5 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
@@ -114,6 +114,13 @@ public class URIUtilCanonicalPathTest
// paths with encoded segments should remain encoded
// canonicalPath() is not responsible for decoding characters
{"%2e%2e/", "%2e%2e/"},
+ {"/%2e%2e/", "/%2e%2e/"},
+
+ // paths with parameters are not elided
+ // canonicalPath() is not responsible for decoding characters
+ {"/foo/.;/bar", "/foo/.;/bar"},
+ {"/foo/..;/bar", "/foo/..;/bar"},
+ {"/foo/..;/..;/bar", "/foo/..;/..;/bar"},
};
ArrayList ret = new ArrayList<>();
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java
index d528cf6d171..d3da1f84105 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java
@@ -24,15 +24,21 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
@@ -49,97 +55,97 @@ public class URLEncodedTest
tests.add(dynamicTest("Initially not empty", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
assertEquals(0, urlEncoded.size());
}));
tests.add(dynamicTest("Not empty after decode(\"\")", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("");
+ UrlEncoded.decodeTo("", urlEncoded, UrlEncoded.ENCODING);
assertEquals(0, urlEncoded.size());
}));
tests.add(dynamicTest("Simple encode", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name1=Value1");
+ UrlEncoded.decodeTo("Name1=Value1", urlEncoded, UrlEncoded.ENCODING);
assertEquals(1, urlEncoded.size(), "simple param size");
- assertEquals("Name1=Value1", urlEncoded.encode(), "simple encode");
+ assertEquals("Name1=Value1", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "simple encode");
assertEquals("Value1", urlEncoded.getString("Name1"), "simple get");
}));
tests.add(dynamicTest("Dangling param", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name2=");
+ UrlEncoded.decodeTo("Name2=", urlEncoded, UrlEncoded.ENCODING);
assertEquals(1, urlEncoded.size(), "dangling param size");
- assertEquals("Name2", urlEncoded.encode(), "dangling encode");
+ assertEquals("Name2", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "dangling encode");
assertEquals("", urlEncoded.getString("Name2"), "dangling get");
}));
tests.add(dynamicTest("noValue param", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name3");
+ UrlEncoded.decodeTo("Name3", urlEncoded, UrlEncoded.ENCODING);
assertEquals(1, urlEncoded.size(), "noValue param size");
- assertEquals("Name3", urlEncoded.encode(), "noValue encode");
+ assertEquals("Name3", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "noValue encode");
assertEquals("", urlEncoded.getString("Name3"), "noValue get");
}));
tests.add(dynamicTest("badly encoded param", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name4=V\u0629lue+4%21");
+ UrlEncoded.decodeTo("Name4=V\u0629lue+4%21", urlEncoded, UrlEncoded.ENCODING);
assertEquals(1, urlEncoded.size(), "encoded param size");
- assertEquals("Name4=V%D8%A9lue+4%21", urlEncoded.encode(), "encoded encode");
+ assertEquals("Name4=V%D8%A9lue+4%21", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "encoded encode");
assertEquals("V\u0629lue 4!", urlEncoded.getString("Name4"), "encoded get");
}));
tests.add(dynamicTest("encoded param 1", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name4=Value%2B4%21");
+ UrlEncoded.decodeTo("Name4=Value%2B4%21", urlEncoded, UrlEncoded.ENCODING);
assertEquals(1, urlEncoded.size(), "encoded param size");
- assertEquals("Name4=Value%2B4%21", urlEncoded.encode(), "encoded encode");
+ assertEquals("Name4=Value%2B4%21", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "encoded encode");
assertEquals("Value+4!", urlEncoded.getString("Name4"), "encoded get");
}));
tests.add(dynamicTest("encoded param 2", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name4=Value+4%21%20%214");
+ UrlEncoded.decodeTo("Name4=Value+4%21%20%214", urlEncoded, UrlEncoded.ENCODING);
assertEquals(1, urlEncoded.size(), "encoded param size");
- assertEquals("Name4=Value+4%21+%214", urlEncoded.encode(), "encoded encode");
+ assertEquals("Name4=Value+4%21+%214", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "encoded encode");
assertEquals("Value 4! !4", urlEncoded.getString("Name4"), "encoded get");
}));
tests.add(dynamicTest("multi param", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name5=aaa&Name6=bbb");
+ UrlEncoded.decodeTo("Name5=aaa&Name6=bbb", urlEncoded, UrlEncoded.ENCODING);
assertEquals(2, urlEncoded.size(), "multi param size");
- assertTrue(urlEncoded.encode().equals("Name5=aaa&Name6=bbb") ||
- urlEncoded.encode().equals("Name6=bbb&Name5=aaa"),
- "multi encode " + urlEncoded.encode());
+ assertTrue(UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false).equals("Name5=aaa&Name6=bbb") ||
+ UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false).equals("Name6=bbb&Name5=aaa"),
+ "multi encode " + UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false));
assertEquals("aaa", urlEncoded.getString("Name5"), "multi get");
assertEquals("bbb", urlEncoded.getString("Name6"), "multi get");
}));
tests.add(dynamicTest("multiple value encoded", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name7=aaa&Name7=b%2Cb&Name7=ccc");
- assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", urlEncoded.encode(), "multi encode");
+ UrlEncoded.decodeTo("Name7=aaa&Name7=b%2Cb&Name7=ccc", urlEncoded, UrlEncoded.ENCODING);
+ assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "multi encode");
assertEquals("aaa,b,b,ccc", urlEncoded.getString("Name7"), "list get all");
assertEquals("aaa", urlEncoded.getValues("Name7").get(0), "list get");
assertEquals("b,b", urlEncoded.getValues("Name7").get(1), "list get");
@@ -148,11 +154,11 @@ public class URLEncodedTest
tests.add(dynamicTest("encoded param", () ->
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
urlEncoded.clear();
- urlEncoded.decode("Name8=xx%2C++yy++%2Czz");
+ UrlEncoded.decodeTo("Name8=xx%2C++yy++%2Czz", urlEncoded, UrlEncoded.ENCODING);
assertEquals(1, urlEncoded.size(), "encoded param size");
- assertEquals("Name8=xx%2C++yy++%2Czz", urlEncoded.encode(), "encoded encode");
+ assertEquals("Name8=xx%2C++yy++%2Czz", UrlEncoded.encode(urlEncoded,UrlEncoded.ENCODING, false), "encoded encode");
assertEquals("xx, yy ,zz", urlEncoded.getString("Name8"), "encoded get");
}));
@@ -219,7 +225,7 @@ public class URLEncodedTest
{
try (ByteArrayInputStream in3 = new ByteArrayInputStream("name=libell%E9".getBytes(StringUtil.__ISO_8859_1)))
{
- MultiMap m3 = new MultiMap();
+ MultiMap m3 = new MultiMap<>();
Charset nullCharset = null; // use the one from the system property
UrlEncoded.decodeTo(in3, m3, nullCharset, -1, -1);
assertEquals("libell\u00E9", m3.getString("name"), "stream name");
@@ -230,11 +236,11 @@ public class URLEncodedTest
public void testUtf8()
throws Exception
{
- UrlEncoded urlEncoded = new UrlEncoded();
+ MultiMap urlEncoded = new MultiMap<>();
assertEquals(0, urlEncoded.size(), "Empty");
urlEncoded.clear();
- urlEncoded.decode("text=%E0%B8%9F%E0%B8%AB%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%81%E0%B8%9F%E0%B8%A7%E0%B8%AB%E0%B8%AA%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%AB%E0%B8%9F%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%AA%E0%B8%B2%E0%B8%9F%E0%B8%81%E0%B8%AB%E0%B8%A3%E0%B8%94%E0%B9%89%E0%B8%9F%E0%B8%AB%E0%B8%99%E0%B8%81%E0%B8%A3%E0%B8%94%E0%B8%B5&Action=Submit");
+ UrlEncoded.decodeTo("text=%E0%B8%9F%E0%B8%AB%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%81%E0%B8%9F%E0%B8%A7%E0%B8%AB%E0%B8%AA%E0%B8%94%E0%B8%B2%E0%B9%88%E0%B8%AB%E0%B8%9F%E0%B8%81%E0%B8%A7%E0%B8%94%E0%B8%AA%E0%B8%B2%E0%B8%9F%E0%B8%81%E0%B8%AB%E0%B8%A3%E0%B8%94%E0%B9%89%E0%B8%9F%E0%B8%AB%E0%B8%99%E0%B8%81%E0%B8%A3%E0%B8%94%E0%B8%B5&Action=Submit", urlEncoded, UrlEncoded.ENCODING);
String hex = "E0B89FE0B8ABE0B881E0B8A7E0B894E0B8B2E0B988E0B881E0B89FE0B8A7E0B8ABE0B8AAE0B894E0B8B2E0B988E0B8ABE0B89FE0B881E0B8A7E0B894E0B8AAE0B8B2E0B89FE0B881E0B8ABE0B8A3E0B894E0B989E0B89FE0B8ABE0B899E0B881E0B8A3E0B894E0B8B5";
String expected = new String(TypeUtil.fromHexString(hex), "utf-8");
@@ -245,8 +251,8 @@ public class URLEncodedTest
public void testUtf8MultiByteCodePoint()
{
String input = "text=test%C3%A4";
- UrlEncoded urlEncoded = new UrlEncoded();
- urlEncoded.decode(input);
+ MultiMap urlEncoded = new MultiMap<>();
+ UrlEncoded.decodeTo(input, urlEncoded, UrlEncoded.ENCODING);
// http://www.ltg.ed.ac.uk/~richard/utf-8.cgi?input=00e4&mode=hex
// Should be "testä"
@@ -255,4 +261,50 @@ public class URLEncodedTest
String expected = "test\u00e4";
assertThat(urlEncoded.getString("text"), is(expected));
}
+
+ public static Stream invalidTestData()
+ {
+ ArrayList data = new ArrayList<>();
+ data.add(Arguments.of("Name=xx%zzyy", UTF_8, IllegalArgumentException.class));
+ data.add(Arguments.of("Name=%FF%FF%FF", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=%EF%EF%EF", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=%E%F%F", UTF_8, IllegalArgumentException.class));
+ data.add(Arguments.of("Name=x%", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=x%2", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("Name=xxx%", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ data.add(Arguments.of("name=X%c0%afZ", UTF_8, Utf8Appendable.NotUtf8Exception.class));
+ return data.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidTestData")
+ public void testInvalidDecode(String inputString, Charset charset, Class extends Throwable> expectedThrowable)
+ {
+ assertThrows(expectedThrowable, () ->
+ {
+ UrlEncoded.decodeTo(inputString, new MultiMap<>(), charset);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidTestData")
+ public void testInvalidDecodeUtf8ToMap(String inputString, Charset charset, Class extends Throwable> expectedThrowable)
+ {
+ assertThrows(expectedThrowable, () ->
+ {
+ MultiMap map = new MultiMap<>();
+ UrlEncoded.decodeUtf8To(inputString, map);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidTestData")
+ public void testInvalidDecodeTo(String inputString, Charset charset, Class extends Throwable> expectedThrowable)
+ {
+ assertThrows(expectedThrowable, () ->
+ {
+ MultiMap map = new MultiMap<>();
+ UrlEncoded.decodeTo(inputString, map, charset);
+ });
+ }
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedInvalidEncodingTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedInvalidEncodingTest.java
deleted file mode 100644
index 4df751118c9..00000000000
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/UrlEncodedInvalidEncodingTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package org.eclipse.jetty.util;
-
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-public class UrlEncodedInvalidEncodingTest
-{
- public static Stream data()
- {
- ArrayList data = new ArrayList<>();
- data.add(Arguments.of("Name=xx%zzyy", UTF_8, IllegalArgumentException.class));
- data.add(Arguments.of("Name=%FF%FF%FF", UTF_8, Utf8Appendable.NotUtf8Exception.class));
- data.add(Arguments.of("Name=%EF%EF%EF", UTF_8, Utf8Appendable.NotUtf8Exception.class));
- data.add(Arguments.of("Name=%E%F%F", UTF_8, IllegalArgumentException.class));
- data.add(Arguments.of("Name=x%", UTF_8, Utf8Appendable.NotUtf8Exception.class));
- data.add(Arguments.of("Name=x%2", UTF_8, Utf8Appendable.NotUtf8Exception.class));
- data.add(Arguments.of("Name=xxx%", UTF_8, Utf8Appendable.NotUtf8Exception.class));
- data.add(Arguments.of("name=X%c0%afZ", UTF_8, Utf8Appendable.NotUtf8Exception.class));
- return data.stream();
- }
-
- @ParameterizedTest
- @MethodSource("data")
- public void testDecode(String inputString, Charset charset, Class extends Throwable> expectedThrowable)
- {
- assertThrows(expectedThrowable, () ->
- {
- UrlEncoded urlEncoded = new UrlEncoded();
- urlEncoded.decode(inputString, charset);
- });
- }
-
- @ParameterizedTest
- @MethodSource("data")
- public void testDecodeUtf8ToMap(String inputString, Charset charset, Class extends Throwable> expectedThrowable)
- {
- assertThrows(expectedThrowable, () ->
- {
- MultiMap map = new MultiMap<>();
- UrlEncoded.decodeUtf8To(inputString, map);
- });
- }
-
- @ParameterizedTest
- @MethodSource("data")
- public void testDecodeTo(String inputString, Charset charset, Class extends Throwable> expectedThrowable)
- {
- assertThrows(expectedThrowable, () ->
- {
- MultiMap map = new MultiMap<>();
- UrlEncoded.decodeTo(inputString, map, charset);
- });
- }
-}
diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
index 284d1332247..a494972dc14 100644
--- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
+++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
@@ -1314,7 +1314,6 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
setClassLoader(null);
}
- setAvailable(true);
_unavailableException = null;
}
}
diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java
index 44c07a2815b..57a7548e078 100644
--- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java
+++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java
@@ -40,8 +40,8 @@ import org.slf4j.LoggerFactory;
public abstract class JavaxWebSocketContainer extends ContainerLifeCycle implements javax.websocket.WebSocketContainer
{
private static final Logger LOG = LoggerFactory.getLogger(JavaxWebSocketContainer.class);
- private final SessionTracker sessionTracker = new SessionTracker();
private final List sessionListeners = new ArrayList<>();
+ protected final SessionTracker sessionTracker = new SessionTracker();
protected final Configuration.ConfigurationCustomizer defaultCustomizer = new Configuration.ConfigurationCustomizer();
protected final WebSocketComponents components;
diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java
index 600540259a3..ac4093760a6 100644
--- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java
+++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java
@@ -18,26 +18,26 @@
package org.eclipse.jetty.websocket.javax.common;
-import java.io.IOException;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.CloseReason;
import javax.websocket.Session;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.eclipse.jetty.util.component.Graceful;
-public class SessionTracker extends AbstractLifeCycle implements JavaxWebSocketSessionListener
+public class SessionTracker extends AbstractLifeCycle implements JavaxWebSocketSessionListener, Graceful
{
- private static final Logger LOG = LoggerFactory.getLogger(SessionTracker.class);
-
- private final CopyOnWriteArraySet sessions = new CopyOnWriteArraySet<>();
+ private final Set sessions = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ private boolean isShutdown = false;
public Set getSessions()
{
- return Collections.unmodifiableSet(sessions);
+ return Set.copyOf(sessions);
}
@Override
@@ -52,22 +52,40 @@ public class SessionTracker extends AbstractLifeCycle implements JavaxWebSocketS
sessions.remove(session);
}
+ @Override
+ protected void doStart() throws Exception
+ {
+ isShutdown = false;
+ super.doStart();
+ }
+
@Override
protected void doStop() throws Exception
{
- for (Session session : sessions)
+ sessions.clear();
+ super.doStop();
+ }
+
+ @Override
+ public CompletableFuture shutdown()
+ {
+ isShutdown = true;
+ return Graceful.shutdown(() ->
{
- try
+ for (Session session : sessions)
{
+ if (Thread.interrupted())
+ break;
+
// GOING_AWAY is abnormal close status so it will hard close connection after sent.
session.close(new CloseReason(CloseReason.CloseCodes.GOING_AWAY, "Container being shut down"));
}
- catch (IOException e)
- {
- LOG.trace("IGNORED", e);
- }
- }
+ });
+ }
- super.doStop();
+ @Override
+ public boolean isShutdown()
+ {
+ return isShutdown;
}
}
diff --git a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EchoSocket.java b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EchoSocket.java
new file mode 100644
index 00000000000..c18d8b665a4
--- /dev/null
+++ b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EchoSocket.java
@@ -0,0 +1,43 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under
+// the terms of the Eclipse Public License 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0
+//
+// This Source Code may also be made available under the following
+// Secondary Licenses when the conditions for such availability set
+// forth in the Eclipse Public License, v. 2.0 are satisfied:
+// the Apache License v2.0 which is available at
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.javax.tests;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import javax.websocket.ClientEndpoint;
+import javax.websocket.server.ServerEndpoint;
+
+@ServerEndpoint("/")
+@ClientEndpoint
+public class EchoSocket extends EventSocket
+{
+ @Override
+ public void onMessage(String message) throws IOException
+ {
+ super.onMessage(message);
+ session.getBasicRemote().sendText(message);
+ }
+
+ @Override
+ public void onMessage(ByteBuffer message) throws IOException
+ {
+ super.onMessage(message);
+ session.getBasicRemote().sendBinary(message);
+ }
+}
diff --git a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EventSocket.java b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EventSocket.java
index a8b7d1a5fe9..a69db90ba45 100644
--- a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EventSocket.java
+++ b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EventSocket.java
@@ -98,23 +98,4 @@ public class EventSocket
error = cause;
errorLatch.countDown();
}
-
- @ServerEndpoint("/")
- @ClientEndpoint
- public static class EchoSocket extends EventSocket
- {
- @Override
- public void onMessage(String message) throws IOException
- {
- super.onMessage(message);
- session.getBasicRemote().sendText(message);
- }
-
- @Override
- public void onMessage(ByteBuffer message) throws IOException
- {
- super.onMessage(message);
- session.getBasicRemote().sendBinary(message);
- }
- }
}
diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/GracefulCloseTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/GracefulCloseTest.java
new file mode 100644
index 00000000000..b2745345484
--- /dev/null
+++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/GracefulCloseTest.java
@@ -0,0 +1,133 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under
+// the terms of the Eclipse Public License 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0
+//
+// This Source Code may also be made available under the following
+// Secondary Licenses when the conditions for such availability set
+// forth in the Eclipse Public License, v. 2.0 are satisfied:
+// the Apache License v2.0 which is available at
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.javax.tests;
+
+import java.net.URI;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import javax.websocket.CloseReason;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.component.Graceful;
+import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer;
+import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GracefulCloseTest
+{
+ private static final BlockingArrayQueue serverEndpoints = new BlockingArrayQueue<>();
+ private Server server;
+ private URI serverUri;
+ private JavaxWebSocketClientContainer client;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ ServletContextHandler contextHandler = new ServletContextHandler();
+ contextHandler.setContextPath("/");
+ server.setHandler(contextHandler);
+ JavaxWebSocketServletContainerInitializer.configure(contextHandler, (context, container) ->
+ container.addEndpoint(ServerSocket.class));
+ server.start();
+ serverUri = WSURI.toWebsocket(server.getURI());
+
+ // StopTimeout is necessary for the websocket server sessions to gracefully close.
+ server.setStopTimeout(1000);
+
+ client = new JavaxWebSocketClientContainer();
+ client.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ client.stop();
+ server.stop();
+ }
+
+ @ServerEndpoint("/")
+ public static class ServerSocket extends EchoSocket
+ {
+ @Override
+ public void onOpen(Session session, EndpointConfig endpointConfig)
+ {
+ serverEndpoints.add(this);
+ super.onOpen(session, endpointConfig);
+ }
+ }
+
+ @Test
+ public void testClientStop() throws Exception
+ {
+ EventSocket clientEndpoint = new EventSocket();
+ client.connectToServer(clientEndpoint, serverUri);
+ EventSocket serverEndpoint = Objects.requireNonNull(serverEndpoints.poll(5, TimeUnit.SECONDS));
+
+ // There is no API for a Javax WebSocketContainer stop timeout.
+ Graceful.shutdown(client).get(5, TimeUnit.SECONDS);
+ client.stop();
+
+ // Check that the client endpoint was closed with the correct status code and no error.
+ assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(clientEndpoint.closeReason.getCloseCode(), is(CloseReason.CloseCodes.GOING_AWAY));
+ assertNull(clientEndpoint.error);
+
+ // Check that the server endpoint was closed with the correct status code and no error.
+ assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(serverEndpoint.closeReason.getCloseCode(), is(CloseReason.CloseCodes.GOING_AWAY));
+ assertNull(serverEndpoint.error);
+ }
+
+ @Test
+ public void testServerStop() throws Exception
+ {
+ EventSocket clientEndpoint = new EventSocket();
+ client.connectToServer(clientEndpoint, serverUri);
+ EventSocket serverEndpoint = Objects.requireNonNull(serverEndpoints.poll(5, TimeUnit.SECONDS));
+
+ server.stop();
+
+ // Check that the client endpoint was closed with the correct status code and no error.
+ assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(clientEndpoint.closeReason.getCloseCode(), is(CloseReason.CloseCodes.GOING_AWAY));
+ assertNull(clientEndpoint.error);
+
+ // Check that the server endpoint was closed with the correct status code and no error.
+ assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(serverEndpoint.closeReason.getCloseCode(), is(CloseReason.CloseCodes.GOING_AWAY));
+ assertNull(serverEndpoint.error);
+ }
+}
diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java
index cc03423accd..88657bbbd74 100644
--- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java
+++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java
@@ -40,7 +40,7 @@ import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession;
import org.eclipse.jetty.websocket.javax.common.encoders.AvailableEncoders;
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
-import org.eclipse.jetty.websocket.javax.tests.EventSocket.EchoSocket;
+import org.eclipse.jetty.websocket.javax.tests.EchoSocket;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
diff --git a/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java b/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java
index da4760d655f..9abdbbed8af 100644
--- a/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java
+++ b/jetty-websocket/websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java
@@ -29,6 +29,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.eclipse.jetty.client.HttpClient;
@@ -38,6 +39,7 @@ import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ShutdownThread;
import org.eclipse.jetty.websocket.api.Session;
@@ -68,6 +70,7 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
private final Configuration.ConfigurationCustomizer configurationCustomizer = new Configuration.ConfigurationCustomizer();
private final WebSocketComponents components = new WebSocketComponents();
private boolean stopAtShutdown = false;
+ private long _stopTimeout = Long.MAX_VALUE;
/**
* Instantiate a WebSocketClient with defaults
@@ -388,11 +391,33 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
stopAtShutdown = stop;
}
+ /**
+ * The timeout to allow all remaining open Sessions to be closed gracefully using the close code {@link org.eclipse.jetty.websocket.api.StatusCode#SHUTDOWN}.
+ * @param stopTimeout the time in ms to wait for the graceful close, use a value less than or equal to 0 to not gracefully close.
+ */
+ public void setStopTimeout(long stopTimeout)
+ {
+ _stopTimeout = stopTimeout;
+ }
+
+ public long getStopTimeout()
+ {
+ return _stopTimeout;
+ }
+
public boolean isStopAtShutdown()
{
return stopAtShutdown;
}
+ @Override
+ protected void doStop() throws Exception
+ {
+ if (getStopTimeout() > 0)
+ Graceful.shutdown(this).get(getStopTimeout(), TimeUnit.MILLISECONDS);
+ super.doStop();
+ }
+
@Override
public String toString()
{
diff --git a/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java b/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java
index e963800ae9f..5a7d4ea44b5 100644
--- a/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java
+++ b/jetty-websocket/websocket-jetty-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java
@@ -19,21 +19,28 @@
package org.eclipse.jetty.websocket.common;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
-public class SessionTracker extends AbstractLifeCycle implements WebSocketSessionListener
+public class SessionTracker extends AbstractLifeCycle implements WebSocketSessionListener, Graceful
{
- private List sessions = new CopyOnWriteArrayList<>();
+ private final Set sessions = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ private boolean isShutdown = false;
public Collection getSessions()
{
- return sessions;
+ return Set.copyOf(sessions);
}
@Override
@@ -48,15 +55,40 @@ public class SessionTracker extends AbstractLifeCycle implements WebSocketSessio
sessions.remove(session);
}
+ @Override
+ protected void doStart() throws Exception
+ {
+ isShutdown = false;
+ super.doStart();
+ }
+
@Override
protected void doStop() throws Exception
{
- for (Session session : sessions)
- {
- // SHUTDOWN is abnormal close status so it will hard close connection after sent.
- session.close(StatusCode.SHUTDOWN, "Container being shut down");
- }
-
+ sessions.clear();
super.doStop();
}
+
+ @Override
+ public CompletableFuture shutdown()
+ {
+ isShutdown = true;
+ return Graceful.shutdown(() ->
+ {
+ for (Session session : sessions)
+ {
+ if (Thread.interrupted())
+ break;
+
+ // SHUTDOWN is abnormal close status so it will hard close connection after sent.
+ session.close(StatusCode.SHUTDOWN, "Container being shut down");
+ }
+ });
+ }
+
+ @Override
+ public boolean isShutdown()
+ {
+ return isShutdown;
+ }
}
diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java
index ed0c40b07fa..5eadef61ef3 100644
--- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java
+++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java
@@ -119,6 +119,7 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
frameHandlerFactory = factory;
addSessionListener(sessionTracker);
+ addBean(sessionTracker);
}
public void addMapping(String pathSpec, JettyWebSocketCreator creator)
diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/GracefulCloseTest.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/GracefulCloseTest.java
new file mode 100644
index 00000000000..d3157d0f283
--- /dev/null
+++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/GracefulCloseTest.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under
+// the terms of the Eclipse Public License 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0
+//
+// This Source Code may also be made available under the following
+// Secondary Licenses when the conditions for such availability set
+// forth in the Eclipse Public License, v. 2.0 are satisfied:
+// the Apache License v2.0 which is available at
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.tests;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.util.WSURI;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GracefulCloseTest
+{
+ private final EventSocket serverEndpoint = new EchoSocket();
+ private Server server;
+ private URI serverUri;
+ private WebSocketClient client;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ ServletContextHandler contextHandler = new ServletContextHandler();
+ contextHandler.setContextPath("/");
+ server.setHandler(contextHandler);
+ JettyWebSocketServletContainerInitializer.configure(contextHandler, (context, container) ->
+ container.addMapping("/", ((req, resp) -> serverEndpoint)));
+ server.start();
+ serverUri = WSURI.toWebsocket(server.getURI());
+
+ // StopTimeout is necessary for the websocket server sessions to gracefully close.
+ server.setStopTimeout(1000);
+
+ client = new WebSocketClient();
+ client.setStopTimeout(1000);
+ client.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ client.stop();
+ server.stop();
+ }
+
+ @Test
+ public void testClientStop() throws Exception
+ {
+ EventSocket clientEndpoint = new EventSocket();
+ client.connect(clientEndpoint, serverUri).get(5, TimeUnit.SECONDS);
+
+ client.stop();
+
+ // Check that the client endpoint was closed with the correct status code and no error.
+ assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(clientEndpoint.closeCode, is(StatusCode.SHUTDOWN));
+ assertNull(clientEndpoint.error);
+
+ // Check that the server endpoint was closed with the correct status code and no error.
+ assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(serverEndpoint.closeCode, is(StatusCode.SHUTDOWN));
+ assertNull(serverEndpoint.error);
+ }
+
+ @Test
+ public void testServerStop() throws Exception
+ {
+ EventSocket clientEndpoint = new EventSocket();
+ client.connect(clientEndpoint, serverUri).get(5, TimeUnit.SECONDS);
+
+ server.stop();
+
+ // Check that the client endpoint was closed with the correct status code and no error.
+ assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(clientEndpoint.closeCode, is(StatusCode.SHUTDOWN));
+ assertNull(clientEndpoint.error);
+
+ // Check that the server endpoint was closed with the correct status code and no error.
+ assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
+ assertThat(serverEndpoint.closeCode, is(StatusCode.SHUTDOWN));
+ assertNull(serverEndpoint.error);
+ }
+}
diff --git a/tests/jetty-jmh/pom.xml b/tests/jetty-jmh/pom.xml
index edf3878dfd9..200943c79f1 100644
--- a/tests/jetty-jmh/pom.xml
+++ b/tests/jetty-jmh/pom.xml
@@ -106,6 +106,11 @@
jetty-slf4j-impl
test
+
+ org.eclipse.jetty
+ jetty-client
+ ${project.version}
+
org.eclipse.jetty.toolchain
jetty-test-helper
diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/client/jmh/ConnectionPoolsBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/client/jmh/ConnectionPoolsBenchmark.java
new file mode 100644
index 00000000000..e728bd8d196
--- /dev/null
+++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/client/jmh/ConnectionPoolsBenchmark.java
@@ -0,0 +1,174 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under
+// the terms of the Eclipse Public License 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0
+//
+// This Source Code may also be made available under the following
+// Secondary Licenses when the conditions for such availability set
+// forth in the Eclipse Public License, v. 2.0 are satisfied:
+// the Apache License v2.0 which is available at
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.jmh;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.eclipse.jetty.client.ConnectionPool;
+import org.eclipse.jetty.client.DuplexConnectionPool;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpConversation;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.HttpRequest;
+import org.eclipse.jetty.client.MultiplexConnectionPool;
+import org.eclipse.jetty.client.Origin;
+import org.eclipse.jetty.client.RoundRobinConnectionPool;
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.util.Attachable;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Promise;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+@State(Scope.Benchmark)
+public class ConnectionPoolsBenchmark
+{
+ private ConnectionPool pool;
+
+ @Param({"round-robin", "cached/multiplex", "uncached/multiplex", "cached/duplex", "uncached/duplex"})
+ public static String POOL_TYPE;
+
+ @Setup
+ public void setUp() throws Exception
+ {
+ HttpClient httpClient = new HttpClient()
+ {
+ @Override
+ protected void newConnection(HttpDestination destination, Promise promise)
+ {
+ promise.succeeded(new MockConnection());
+ }
+ };
+ HttpDestination httpDestination = new HttpDestination(httpClient, new Origin("http", "localhost", 8080))
+ {
+ };
+
+ HttpConversation httpConversation = new HttpConversation();
+ HttpRequest httpRequest = new HttpRequest(httpClient, httpConversation, new URI("http://localhost:8080")) {};
+ HttpExchange httpExchange = new HttpExchange(httpDestination, httpRequest, new ArrayList<>());
+ httpDestination.getHttpExchanges().add(httpExchange);
+
+ int initialConnections = 12;
+ int maxConnections = 100;
+ switch (POOL_TYPE)
+ {
+ case "uncached/duplex":
+ pool = new DuplexConnectionPool(httpDestination, maxConnections, false, Callback.NOOP);
+ pool.preCreateConnections(initialConnections).get();
+ break;
+ case "cached/duplex":
+ pool = new DuplexConnectionPool(httpDestination, maxConnections, true, Callback.NOOP);
+ pool.preCreateConnections(initialConnections).get();
+ break;
+ case "uncached/multiplex":
+ pool = new MultiplexConnectionPool(httpDestination, maxConnections,false, Callback.NOOP, 12);
+ pool.preCreateConnections(initialConnections).get();
+ break;
+ case "cached/multiplex":
+ pool = new MultiplexConnectionPool(httpDestination, maxConnections,true, Callback.NOOP, 12);
+ pool.preCreateConnections(initialConnections).get();
+ break;
+ case "round-robin":
+ pool = new RoundRobinConnectionPool(httpDestination, maxConnections, Callback.NOOP);
+ pool.preCreateConnections(maxConnections).get();
+ break;
+ default:
+ throw new AssertionError("Unknown pool type: " + POOL_TYPE);
+ }
+ }
+
+ @TearDown
+ public void tearDown()
+ {
+ pool.close();
+ pool = null;
+ }
+
+ @Benchmark
+ public void testPool()
+ {
+ Connection connection = pool.acquire(true);
+ if (connection == null && !POOL_TYPE.equals("round-robin"))
+ throw new AssertionError("from thread " + Thread.currentThread().getName());
+ Blackhole.consumeCPU(ThreadLocalRandom.current().nextInt(10, 20));
+ if (connection != null)
+ pool.release(connection);
+ }
+
+ public static void main(String[] args) throws RunnerException
+ {
+ Options opt = new OptionsBuilder()
+ .include(ConnectionPoolsBenchmark.class.getSimpleName())
+ .warmupIterations(3)
+ .measurementIterations(3)
+ .forks(1)
+ .threads(12)
+ //.addProfiler(LinuxPerfProfiler.class)
+ .build();
+
+ new Runner(opt).run();
+ }
+
+ static class MockConnection implements Connection, Attachable
+ {
+ private Object attachment;
+
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public boolean isClosed()
+ {
+ return false;
+ }
+
+ @Override
+ public void send(Request request, Response.CompleteListener listener)
+ {
+ }
+
+ @Override
+ public void setAttachment(Object obj)
+ {
+ this.attachment = obj;
+ }
+
+ @Override
+ public Object getAttachment()
+ {
+ return attachment;
+ }
+ }
+}
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/SessionDump.java b/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/SessionDump.java
index 86fdcca40df..67bd337b7c6 100644
--- a/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/SessionDump.java
+++ b/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/SessionDump.java
@@ -79,7 +79,7 @@ public class SessionDump extends HttpServlet
session = request.getSession(true);
session.setAttribute("test", "value");
session.setAttribute("obj", new ObjectAttributeValue(System.currentTimeMillis()));
- session.setAttribute("WEBCL", new MultiMap());
+ session.setAttribute("WEBCL", new MultiMap<>());
}
else if (session != null)
{
@@ -137,7 +137,7 @@ public class SessionDump extends HttpServlet
else
{
if (session.getAttribute("WEBCL") == null)
- session.setAttribute("WEBCL", new MultiMap());
+ session.setAttribute("WEBCL", new MultiMap<>());
try
{
out.println("ID: " + session.getId() + "
");