From 51120b1f0bda1a61e60079a8ab4d73d50700aa51 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Tue, 5 Jan 2021 12:52:34 +0100 Subject: [PATCH] Tries improvements (#5736) * ArrayTrie handles full alphabet * TreeTrie handles case sensitive * improved trie selection from Index builders * optimisations, cleanups and benchmarks. Signed-off-by: Greg Wilkins --- .../org/eclipse/jetty/http/HttpField.java | 16 +- .../eclipse/jetty/http/HttpHeaderValue.java | 89 ++- .../org/eclipse/jetty/http/HttpParser.java | 13 +- .../eclipse/jetty/http/HttpParserTest.java | 79 +++ .../org/eclipse/jetty/http2/HTTP2Cipher.java | 583 ++++++++-------- .../jetty/server/HttpChannelOverHttp.java | 37 +- .../org/eclipse/jetty/server/HttpOutput.java | 4 +- .../org/eclipse/jetty/server/Request.java | 6 +- .../org/eclipse/jetty/util/AbstractTrie.java | 120 ++-- .../eclipse/jetty/util/ArrayTernaryTrie.java | 75 +- .../org/eclipse/jetty/util/ArrayTrie.java | 643 ++++++++++-------- .../org/eclipse/jetty/util/EmptyTrie.java | 67 +- .../java/org/eclipse/jetty/util/Index.java | 133 ++-- .../org/eclipse/jetty/util/StringUtil.java | 116 +++- .../java/org/eclipse/jetty/util/TreeTrie.java | 213 ++++-- .../org/eclipse/jetty/util/IndexTest.java | 50 +- .../java/org/eclipse/jetty/util/TrieTest.java | 441 +++++++----- .../org/eclipse/jetty/util/TrieBenchmark.java | 285 ++++++++ 18 files changed, 1951 insertions(+), 1019 deletions(-) create mode 100644 tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/TrieBenchmark.java diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java index 1fa856e90f0..9032e457159 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java @@ -145,18 +145,16 @@ public class HttpField return false; if (_value == null) return false; - if (search.equals(_value)) + if (search.equalsIgnoreCase(_value)) return true; - search = StringUtil.asciiToLowerCase(search); - int state = 0; int match = 0; int param = 0; for (int i = 0; i < _value.length(); i++) { - char c = _value.charAt(i); + char c = StringUtil.asciiToLowerCase(_value.charAt(i)); switch (state) { case 0: // initial white space @@ -181,7 +179,7 @@ public class HttpField break; default: // character - match = Character.toLowerCase(c) == search.charAt(0) ? 1 : -1; + match = c == StringUtil.asciiToLowerCase(search.charAt(0)) ? 1 : -1; state = 1; break; } @@ -206,7 +204,7 @@ public class HttpField if (match > 0) { if (match < search.length()) - match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1; + match = c == StringUtil.asciiToLowerCase(search.charAt(match)) ? (match + 1) : -1; else if (c != ' ' && c != '\t') match = -1; } @@ -229,7 +227,7 @@ public class HttpField if (match >= 0) { if (match < search.length()) - match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1; + match = c == StringUtil.asciiToLowerCase(search.charAt(match)) ? (match + 1) : -1; else match = -1; } @@ -240,7 +238,7 @@ public class HttpField if (match >= 0) { if (match < search.length()) - match = Character.toLowerCase(c) == search.charAt(match) ? (match + 1) : -1; + match = c == StringUtil.asciiToLowerCase(search.charAt(match)) ? (match + 1) : -1; else match = -1; } @@ -290,7 +288,7 @@ public class HttpField if (param >= 0) { if (param < __zeroquality.length()) - param = Character.toLowerCase(c) == __zeroquality.charAt(param) ? (param + 1) : -1; + param = c == __zeroquality.charAt(param) ? (param + 1) : -1; else if (c != '0' && c != '.') param = -1; } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeaderValue.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeaderValue.java index 650ae8ec87f..f609ac8f5ed 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeaderValue.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeaderValue.java @@ -15,9 +15,11 @@ package org.eclipse.jetty.http; import java.nio.ByteBuffer; import java.util.EnumSet; +import java.util.function.Function; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Index; +import org.eclipse.jetty.util.StringUtil; /** * @@ -71,7 +73,7 @@ public enum HttpHeaderValue return _string; } - private static EnumSet __known = + private static final EnumSet __known = EnumSet.of(HttpHeader.CONNECTION, HttpHeader.TRANSFER_ENCODING, HttpHeader.CONTENT_ENCODING); @@ -82,4 +84,89 @@ public enum HttpHeaderValue return false; return __known.contains(header); } + + /** + * Parse an unquoted comma separated list of index keys. + * @param value A string list of index keys, separated with commas and possible white space + * @param found The function to call for all found index entries. If the function returns false parsing is halted. + * @return true if parsing completed normally and all found index items returned true from the found function. + */ + public static boolean parseCsvIndex(String value, Function found) + { + return parseCsvIndex(value, found, null); + } + + /** + * Parse an unquoted comma separated list of index keys. + * @param value A string list of index keys, separated with commas and possible white space + * @param found The function to call for all found index entries. If the function returns false parsing is halted. + * @param unknown The function to call for foound unknown entries. If the function returns false parsing is halted. + * @return true if parsing completed normally and all found index items returned true from the found function. + */ + public static boolean parseCsvIndex(String value, Function found, Function unknown) + { + if (StringUtil.isBlank(value)) + return true; + int next = 0; + parsing: while (next < value.length()) + { + // Look for the best fit next token + HttpHeaderValue token = CACHE.getBest(value, next, value.length() - next); + + // if a token is found + if (token != null) + { + // check that it is only followed by whatspace, EOL and/or comma + int i = next + token.toString().length(); + loop: while (true) + { + if (i >= value.length()) + return found.apply(token); + switch (value.charAt(i)) + { + case ',': + if (!found.apply(token)) + return false; + next = i + 1; + continue parsing; + case ' ': + break; + default: + break loop; + } + i++; + } + } + + // Token was not correctly matched + if (' ' == value.charAt(next)) + { + next++; + continue; + } + + int comma = value.indexOf(',', next); + if (comma == next) + { + next++; + continue; + } + else if (comma > next) + { + if (unknown == null) + { + next = comma + 1; + continue; + } + String v = value.substring(next, comma).trim(); + if (StringUtil.isBlank(v) || unknown.apply(v)) + { + next = comma + 1; + continue; + } + } + return false; + } + return true; + } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 6a25b962062..10a7f68940f 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -109,7 +109,6 @@ public class HttpParser .with(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip")) .with(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate")) .with(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate, br")) - .with(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip,deflate,sdch")) .with(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-US,enq=0.5")) .with(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-GB,en-USq=0.8,enq=0.6")) .with(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-AU,enq=0.9,it-ITq=0.8,itq=0.7,en-GBq=0.6,en-USq=0.5")) @@ -1058,19 +1057,11 @@ public class HttpParser if (addToFieldCache && _header != null && _valueString != null) { if (_fieldCache == null) - { - _fieldCache = (getHeaderCacheSize() > 0 && (_version != null && _version == HttpVersion.HTTP_1_1)) - ? new Index.Builder() - .caseSensitive(false) - .mutable() - .maxCapacity(getHeaderCacheSize()) - .build() - : NO_CACHE; - } + _fieldCache = Index.buildCaseSensitiveMutableVisibleAsciiAlphabet(getHeaderCacheSize()); if (_field == null) _field = new HttpField(_header, caseInsensitiveHeader(_headerString, _header.asString()), _valueString); - if (!_fieldCache.put(_field)) + if (_field.getValue().length() < getHeaderCacheSize() && !_fieldCache.put(_field)) { _fieldCache.clear(); _fieldCache.put(_field); diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java index e4fcf785650..591805775b6 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java @@ -37,6 +37,7 @@ import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -2971,4 +2972,82 @@ public class HttpParserTest _complianceViolation.add(violation); } } + + @Test + public void testHttpHeaderValueParseCsv() + { + final List list = new ArrayList<>(); + final List unknowns = new ArrayList<>(); + + assertTrue(HttpHeaderValue.parseCsvIndex("", list::add, unknowns::add)); + assertThat(list, empty()); + assertThat(unknowns, empty()); + + assertTrue(HttpHeaderValue.parseCsvIndex(" ", list::add, unknowns::add)); + assertThat(list, empty()); + assertThat(unknowns, empty()); + + assertTrue(HttpHeaderValue.parseCsvIndex(",", list::add, unknowns::add)); + assertThat(list, empty()); + assertThat(unknowns, empty()); + + assertTrue(HttpHeaderValue.parseCsvIndex(",,", list::add, unknowns::add)); + assertThat(list, empty()); + assertThat(unknowns, empty()); + + assertTrue(HttpHeaderValue.parseCsvIndex(" , , ", list::add, unknowns::add)); + assertThat(list, empty()); + assertThat(unknowns, empty()); + + list.clear(); + assertTrue(HttpHeaderValue.parseCsvIndex("close", list::add)); + assertThat(list, contains(HttpHeaderValue.CLOSE)); + + list.clear(); + assertTrue(HttpHeaderValue.parseCsvIndex(" close ", list::add)); + assertThat(list, contains(HttpHeaderValue.CLOSE)); + + list.clear(); + assertTrue(HttpHeaderValue.parseCsvIndex(",close,", list::add)); + assertThat(list, contains(HttpHeaderValue.CLOSE)); + + list.clear(); + assertTrue(HttpHeaderValue.parseCsvIndex(" , close , ", list::add)); + assertThat(list, contains(HttpHeaderValue.CLOSE)); + + list.clear(); + assertTrue(HttpHeaderValue.parseCsvIndex(" close,GZIP, chunked , Keep-Alive ", list::add)); + assertThat(list, contains(HttpHeaderValue.CLOSE, HttpHeaderValue.GZIP, HttpHeaderValue.CHUNKED, HttpHeaderValue.KEEP_ALIVE)); + + list.clear(); + assertTrue(HttpHeaderValue.parseCsvIndex(" close,GZIP, chunked , Keep-Alive ", t -> + { + if (t.toString().startsWith("c")) + list.add(t); + return true; + })); + assertThat(list, contains(HttpHeaderValue.CLOSE, HttpHeaderValue.CHUNKED)); + + list.clear(); + assertFalse(HttpHeaderValue.parseCsvIndex(" close,GZIP, chunked , Keep-Alive ", t -> + { + if (HttpHeaderValue.CHUNKED == t) + return false; + list.add(t); + return true; + })); + assertThat(list, contains(HttpHeaderValue.CLOSE, HttpHeaderValue.GZIP)); + + list.clear(); + unknowns.clear(); + assertTrue(HttpHeaderValue.parseCsvIndex("closed,close, unknown , bytes", list::add, unknowns::add)); + assertThat(list, contains(HttpHeaderValue.CLOSE, HttpHeaderValue.BYTES)); + assertThat(unknowns, contains("closed", "unknown")); + + list.clear(); + unknowns.clear(); + assertFalse(HttpHeaderValue.parseCsvIndex("close, unknown , bytes", list::add, s -> false)); + assertThat(list, contains(HttpHeaderValue.CLOSE)); + assertThat(unknowns, empty()); + } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Cipher.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Cipher.java index 3fc044fe224..b252f23bc9d 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Cipher.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Cipher.java @@ -14,311 +14,312 @@ package org.eclipse.jetty.http2; import java.util.Comparator; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.eclipse.jetty.util.Index; +import org.eclipse.jetty.util.StringUtil; public class HTTP2Cipher { public static final Comparator COMPARATOR = new CipherComparator(); - private static final Index __blackProtocols = new Index.Builder() - .caseSensitive(false) - .with("TLSv1.2", Boolean.TRUE) - .with("TLSv1.1", Boolean.TRUE) - .with("TLSv1", Boolean.TRUE) - .with("SSL", Boolean.TRUE) - .with("SSLv2", Boolean.TRUE) - .with("SSLv3", Boolean.TRUE) - .build(); + private static final Set __blackProtocols = Stream.of( + "TLSv1.2", + "TLSv1.1", + "TLSv1", + "SSL", + "SSLv2", + "SSLv3" + ).map(StringUtil::asciiToUpperCase).collect(Collectors.toSet()); - private static final Index __blackCiphers = new Index.Builder() - .caseSensitive(false) - .with("TLS_NULL_WITH_NULL_NULL", Boolean.TRUE) - .with("TLS_RSA_WITH_NULL_MD5", Boolean.TRUE) - .with("TLS_RSA_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_RSA_EXPORT_WITH_RC4_40_MD5", Boolean.TRUE) - .with("TLS_RSA_WITH_RC4_128_MD5", Boolean.TRUE) - .with("TLS_RSA_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5", Boolean.TRUE) - .with("TLS_RSA_WITH_IDEA_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_WITH_DES_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_DES_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_DES_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_DES_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_DES_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", Boolean.TRUE) - .with("TLS_DH_anon_WITH_RC4_128_MD5", Boolean.TRUE) - .with("TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_WITH_DES_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_KRB5_WITH_DES_CBC_SHA", Boolean.TRUE) - .with("TLS_KRB5_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_KRB5_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_KRB5_WITH_IDEA_CBC_SHA", Boolean.TRUE) - .with("TLS_KRB5_WITH_DES_CBC_MD5", Boolean.TRUE) - .with("TLS_KRB5_WITH_3DES_EDE_CBC_MD5", Boolean.TRUE) - .with("TLS_KRB5_WITH_RC4_128_MD5", Boolean.TRUE) - .with("TLS_KRB5_WITH_IDEA_CBC_MD5", Boolean.TRUE) - .with("TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", Boolean.TRUE) - .with("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", Boolean.TRUE) - .with("TLS_KRB5_EXPORT_WITH_RC4_40_SHA", Boolean.TRUE) - .with("TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", Boolean.TRUE) - .with("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", Boolean.TRUE) - .with("TLS_KRB5_EXPORT_WITH_RC4_40_MD5", Boolean.TRUE) - .with("TLS_PSK_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_WITH_NULL_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_AES_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_AES_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_AES_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", Boolean.TRUE) - .with("TLS_PSK_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_PSK_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_WITH_SEED_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_SEED_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_SEED_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_SEED_CBC_SHA", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_SEED_CBC_SHA", Boolean.TRUE) - .with("TLS_DH_anon_WITH_SEED_CBC_SHA", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_anon_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_PSK_WITH_NULL_SHA256", Boolean.TRUE) - .with("TLS_PSK_WITH_NULL_SHA384", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_NULL_SHA256", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_NULL_SHA384", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_NULL_SHA256", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_NULL_SHA384", Boolean.TRUE) - .with("TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", Boolean.TRUE) - .with("TLS_EMPTY_RENEGOTIATION_INFO_SCSV", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_anon_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_ECDH_anon_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_anon_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDH_anon_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_RC4_128_SHA", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_NULL_SHA", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_NULL_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_NULL_SHA384", Boolean.TRUE) - .with("TLS_RSA_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_RSA_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_PSK_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_PSK_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_PSK_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_PSK_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", Boolean.TRUE) - .with("TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", Boolean.TRUE) - .with("TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_128_CCM", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_256_CCM", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_128_CCM_8", Boolean.TRUE) - .with("TLS_RSA_WITH_AES_256_CCM_8", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_128_CCM", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_256_CCM", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_128_CCM_8", Boolean.TRUE) - .with("TLS_PSK_WITH_AES_256_CCM_8", Boolean.TRUE) - .build(); + private static final Set __blackCiphers = Stream.of( + "TLS_NULL_WITH_NULL_NULL", + "TLS_RSA_WITH_NULL_MD5", + "TLS_RSA_WITH_NULL_SHA", + "TLS_RSA_EXPORT_WITH_RC4_40_MD5", + "TLS_RSA_WITH_RC4_128_MD5", + "TLS_RSA_WITH_RC4_128_SHA", + "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_RSA_WITH_IDEA_CBC_SHA", + "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_RSA_WITH_DES_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_DSS_WITH_DES_CBC_SHA", + "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_RSA_WITH_DES_CBC_SHA", + "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_DSS_WITH_DES_CBC_SHA", + "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_RSA_WITH_DES_CBC_SHA", + "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", + "TLS_DH_anon_WITH_RC4_128_MD5", + "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_anon_WITH_DES_CBC_SHA", + "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_SHA", + "TLS_KRB5_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_RC4_128_SHA", + "TLS_KRB5_WITH_IDEA_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_MD5", + "TLS_KRB5_WITH_3DES_EDE_CBC_MD5", + "TLS_KRB5_WITH_RC4_128_MD5", + "TLS_KRB5_WITH_IDEA_CBC_MD5", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC4_40_SHA", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC4_40_MD5", + "TLS_PSK_WITH_NULL_SHA", + "TLS_DHE_PSK_WITH_NULL_SHA", + "TLS_RSA_PSK_WITH_NULL_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_anon_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_NULL_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_anon_WITH_AES_128_CBC_SHA256", + "TLS_DH_anon_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", + "TLS_PSK_WITH_RC4_128_SHA", + "TLS_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_PSK_WITH_AES_128_CBC_SHA", + "TLS_PSK_WITH_AES_256_CBC_SHA", + "TLS_DHE_PSK_WITH_RC4_128_SHA", + "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_PSK_WITH_RC4_128_SHA", + "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_DSS_WITH_SEED_CBC_SHA", + "TLS_DH_RSA_WITH_SEED_CBC_SHA", + "TLS_DHE_DSS_WITH_SEED_CBC_SHA", + "TLS_DHE_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_anon_WITH_SEED_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DH_anon_WITH_AES_128_GCM_SHA256", + "TLS_DH_anon_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_GCM_SHA256", + "TLS_PSK_WITH_AES_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_CBC_SHA256", + "TLS_PSK_WITH_AES_256_CBC_SHA384", + "TLS_PSK_WITH_NULL_SHA256", + "TLS_PSK_WITH_NULL_SHA384", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_NULL_SHA256", + "TLS_DHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_NULL_SHA256", + "TLS_RSA_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV", + "TLS_ECDH_ECDSA_WITH_NULL_SHA", + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_NULL_SHA", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_RSA_WITH_NULL_SHA", + "TLS_ECDH_RSA_WITH_RC4_128_SHA", + "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_NULL_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_anon_WITH_NULL_SHA", + "TLS_ECDH_anon_WITH_RC4_128_SHA", + "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_RC4_128_SHA", + "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_NULL_SHA", + "TLS_ECDHE_PSK_WITH_NULL_SHA256", + "TLS_ECDHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_AES_128_CCM", + "TLS_RSA_WITH_AES_256_CCM", + "TLS_RSA_WITH_AES_128_CCM_8", + "TLS_RSA_WITH_AES_256_CCM_8", + "TLS_PSK_WITH_AES_128_CCM", + "TLS_PSK_WITH_AES_256_CCM", + "TLS_PSK_WITH_AES_128_CCM_8", + "TLS_PSK_WITH_AES_256_CCM_8" + ).map(StringUtil::asciiToUpperCase).collect(Collectors.toSet()); public static boolean isBlackListProtocol(String tlsProtocol) { - return __blackProtocols.get(tlsProtocol) != null; + return __blackProtocols.contains(StringUtil.asciiToUpperCase(tlsProtocol)); } public static boolean isBlackListCipher(String tlsCipher) { - return __blackCiphers.get(tlsCipher) != null; + return __blackCiphers.contains(StringUtil.asciiToUpperCase(tlsCipher)); } /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java index 5bd69a10863..687995cc90d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java @@ -500,31 +500,24 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque case EXPECT: { - if (HttpVersion.HTTP_1_1.equals(_requestBuilder.version())) + if (!HttpHeaderValue.parseCsvIndex(value, t -> { - HttpHeaderValue expect = HttpHeaderValue.CACHE.get(value); - if (expect == HttpHeaderValue.CONTINUE) + switch (t) { - _expect100Continue = true; - } - else if (expect == HttpHeaderValue.PROCESSING) - { - _expect102Processing = true; - } - else - { - String[] values = field.getValues(); - for (int i = 0; values != null && i < values.length; i++) - { - expect = HttpHeaderValue.CACHE.get(values[i].trim()); - if (expect == HttpHeaderValue.CONTINUE) - _expect100Continue = true; - else if (expect == HttpHeaderValue.PROCESSING) - _expect102Processing = true; - else - _unknownExpectation = true; - } + case CONTINUE: + _expect100Continue = true; + return true; + case PROCESSING: + _expect102Processing = true; + return true; + default: + return false; } + }, s -> false)) + { + _unknownExpectation = true; + _expect100Continue = false; + _expect102Processing = false; } break; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 44c54be7cdc..b4d5e850415 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -171,7 +171,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable } } - private static Logger LOG = LoggerFactory.getLogger(HttpOutput.class); + private static final Logger LOG = LoggerFactory.getLogger(HttpOutput.class); private static final ThreadLocal _encoder = new ThreadLocal<>(); private final HttpChannel _channel; @@ -1019,6 +1019,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (isClosed()) throw new IOException("Closed"); + s = String.valueOf(s); + String charset = _channel.getResponse().getCharacterEncoding(); CharsetEncoder encoder = _encoder.get(); if (encoder == null || !encoder.charset().name().equalsIgnoreCase(charset)) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 0658e1edc38..a1a3c56253c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -288,7 +288,7 @@ public class Request implements HttpServletRequest return !isPush() && getHttpChannel().getHttpTransport().isPushSupported(); } - private static EnumSet NOT_PUSHED_HEADERS = EnumSet.of( + private static final EnumSet NOT_PUSHED_HEADERS = EnumSet.of( HttpHeader.IF_MATCH, HttpHeader.IF_RANGE, HttpHeader.IF_UNMODIFIED_SINCE, @@ -853,7 +853,7 @@ public class Request implements HttpServletRequest public long getDateHeader(String name) { HttpFields fields = _httpFields; - return fields == null ? null : fields.getDateField(name); + return fields == null ? -1 : fields.getDateField(name); } @Override @@ -1062,7 +1062,7 @@ public class Request implements HttpServletRequest List vals = getParameters().getValues(name); if (vals == null) return null; - return vals.toArray(new String[vals.size()]); + return vals.toArray(new String[0]); } public MultiMap getQueryParameters() diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/AbstractTrie.java b/jetty-util/src/main/java/org/eclipse/jetty/util/AbstractTrie.java index 6ea1cf527c8..99f3176fbfa 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/AbstractTrie.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/AbstractTrie.java @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -31,16 +32,21 @@ import java.util.stream.Collectors; */ abstract class AbstractTrie implements Index.Mutable { - final boolean _caseInsensitive; + final boolean _caseSensitive; - protected AbstractTrie(boolean insensitive) + protected AbstractTrie(boolean caseSensitive) { - _caseInsensitive = insensitive; + _caseSensitive = caseSensitive; } public boolean isCaseInsensitive() { - return _caseInsensitive; + return !_caseSensitive; + } + + public boolean isCaseSensitive() + { + return _caseSensitive; } public boolean put(V v) @@ -84,16 +90,27 @@ abstract class AbstractTrie implements Index.Mutable *
  • utf16
  • *
  • utf8
  • * - * The tree has 10 nodes as follows: + * The tree switching by character is: *
    -     *                     1 - 6
    -     *                   /
    -     *                 _ - 8
    -     *               /
    -     *     u - t - f - 1 - 6
    -     *               \
    -     *                 8
    +     *                            1 - 6
    +     *                          /
    +     *                        _ - 8
    +     *                      /
    +     *     root - u - t - f - 1 - 6
    +     *                      \
    +     *                        8
          * 
    + * The count also applies to ternary trees as follows: + *
    +     *     root - u - t - f - _ ----- 1 - 6
    +     *                         \       \
    +     *                          1 - 6   8
    +     *                           \
    +     *                            8
    +     * 
    + * In both cases above there are 10 character nodes plus the root node that can + * hold a value for the empty string key, so the returned capacity is 11. + * * @param keys The keys to be put in a Trie * @param caseSensitive true if the capacity should be calculated with case-sensitive keys * @return The capacity in nodes of a tree decomposition @@ -104,7 +121,7 @@ abstract class AbstractTrie implements Index.Mutable ? new ArrayList<>(keys) : keys.stream().map(String::toLowerCase).collect(Collectors.toList()); Collections.sort(list); - return AbstractTrie.requiredCapacity(list, 0, list.size(), 0); + return 1 + AbstractTrie.requiredCapacity(list, 0, list.size(), 0); } /** @@ -119,45 +136,62 @@ abstract class AbstractTrie implements Index.Mutable { int required = 0; - // Examine all the keys in the subtree - Character nodeChar = null; - for (int i = 0; i < length; i++) + while (true) { - String k = keys.get(offset + i); + // Examine all the keys in the subtree + Character nodeChar = null; + for (int i = 0; i < length; i++) + { + String k = keys.get(offset + i); - // If the key is shorter than our current index then ignore it - if (k.length() <= index) - continue; + // If the key is shorter than our current index then ignore it + if (k.length() <= index) + continue; - // Get the character at the index of the current key - char c = k.charAt(index); + // Get the character at the index of the current key + char c = k.charAt(index); - // If the character is the same as the current node, then we are - // still in the current node and need to continue searching for the - // next node or the end of the keys - if (nodeChar != null && c == nodeChar) - continue; + // If the character is the same as the current node, then we are + // still in the current node and need to continue searching for the + // next node or the end of the keys + if (nodeChar != null && c == nodeChar) + continue; - // The character is a new node, so increase required by 1 - required++; + // The character is a new node, so increase required by 1 + required++; - // if we had a previous node, then add the required nodes for the subtree under it. + // if we had a previous node, then add the required nodes for the subtree under it. + if (nodeChar != null) + required += AbstractTrie.requiredCapacity(keys, offset, i, index + 1); + + // set the char for the new node + nodeChar = c; + + // reset the offset, length and index to continue iteration from the start of the new node + offset += i; + length -= i; + i = 0; + } + + // If we finish the iteration with a nodeChar, then we must add the required nodes for the subtree under it. if (nodeChar != null) - required += AbstractTrie.requiredCapacity(keys, offset, i, index + 1); + { + // instead of recursion here, we loop to avoid tail recursion + index++; + continue; + } - // set the char for the new node - nodeChar = c; - - // reset the offset, length and index to continue iteration from the start of the new node - offset += i; - length -= i; - i = 0; + return required; } + } - // If we finish the iteration with a nodeChar, then we must add the required nodes for the subtree under it. - if (nodeChar != null) - required += AbstractTrie.requiredCapacity(keys, offset, length, index + 1); - - return required; + protected boolean putAll(Map contents) + { + for (Map.Entry entry : contents.entrySet()) + { + if (!put(entry.getKey(), entry.getValue())) + return false; + } + return true; } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTernaryTrie.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTernaryTrie.java index 55498275e5d..01fe5b22a65 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTernaryTrie.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTernaryTrie.java @@ -53,6 +53,7 @@ import java.util.Set; * * @param the Entry type */ +@Deprecated class ArrayTernaryTrie extends AbstractTrie { private static final int LO = 1; @@ -70,7 +71,7 @@ class ArrayTernaryTrie extends AbstractTrie * the 16 bit indexes can overflow and the trie * cannot find existing entries anymore. */ - private static final int MAX_CAPACITY = 21_000; + private static final int MAX_CAPACITY = Character.MAX_VALUE; /** * The Trie rows in a single array which allows a lookup of row,character @@ -99,18 +100,15 @@ class ArrayTernaryTrie extends AbstractTrie /** * Create a Trie * - * @param insensitive true if the Trie is insensitive to the case of the key. + * @param caseSensitive true if the Trie is insensitive to the case of the key. * @param capacity The capacity of the Trie, which is in the worst case * is the total number of characters of all keys stored in the Trie. - * The capacity needed is dependent of the shared prefixes of the keys. - * For example, a capacity of 6 nodes is required to store keys "foo" - * and "bar", but a capacity of only 4 is required to - * store "bar" and "bat". + * @see AbstractTrie#requiredCapacity(Set, boolean) */ @SuppressWarnings("unchecked") - ArrayTernaryTrie(boolean insensitive, int capacity) + ArrayTernaryTrie(boolean caseSensitive, int capacity) { - super(insensitive); + super(caseSensitive); if (capacity > MAX_CAPACITY) throw new IllegalArgumentException("ArrayTernaryTrie maximum capacity overflow (" + capacity + " > " + MAX_CAPACITY + ")"); _value = (V[])new Object[capacity]; @@ -118,28 +116,6 @@ class ArrayTernaryTrie extends AbstractTrie _key = new String[capacity]; } - @SuppressWarnings("unchecked") - ArrayTernaryTrie(boolean insensitive, Map initialValues) - { - super(insensitive); - // The calculated requiredCapacity does not take into account the - // extra reserved slot for the empty string key, nor the slots - // required for 'terminating' the entry (1 slot per key) so we - // have to add those. - Set keys = initialValues.keySet(); - int capacity = AbstractTrie.requiredCapacity(keys, !insensitive) + keys.size() + 1; - if (capacity > MAX_CAPACITY) - throw new IllegalArgumentException("ArrayTernaryTrie maximum capacity overflow (" + capacity + " > " + MAX_CAPACITY + ")"); - _value = (V[])new Object[capacity]; - _tree = new char[capacity * ROW_SIZE]; - _key = new String[capacity]; - for (Map.Entry entry : initialValues.entrySet()) - { - if (!put(entry.getKey(), entry.getValue())) - throw new AssertionError("Invalid capacity calculated (" + capacity + ") at '" + entry + "' for " + initialValues); - } - } - @Override public void clear() { @@ -158,8 +134,8 @@ class ArrayTernaryTrie extends AbstractTrie for (int k = 0; k < limit; k++) { char c = s.charAt(k); - if (isCaseInsensitive() && c < 128) - c = StringUtil.lowercases[c]; + if (isCaseInsensitive()) + c = StringUtil.asciiToLowerCase(c); while (true) { @@ -169,7 +145,7 @@ class ArrayTernaryTrie extends AbstractTrie if (t == _rows) { _rows++; - if (_rows >= _key.length) + if (_rows > _key.length) { _rows--; return false; @@ -202,7 +178,7 @@ class ArrayTernaryTrie extends AbstractTrie if (t == _rows) { _rows++; - if (_rows >= _key.length) + if (_rows > _key.length) { _rows--; return false; @@ -223,8 +199,8 @@ class ArrayTernaryTrie extends AbstractTrie for (int i = 0; i < len; ) { char c = s.charAt(offset + i++); - if (isCaseInsensitive() && c < 128) - c = StringUtil.lowercases[c]; + if (isCaseInsensitive()) + c = StringUtil.asciiToLowerCase(c); while (true) { @@ -259,7 +235,7 @@ class ArrayTernaryTrie extends AbstractTrie { byte c = (byte)(b.get(offset + i++) & 0x7f); if (isCaseInsensitive()) - c = (byte)StringUtil.lowercases[c]; + c = StringUtil.asciiToLowerCase(c); while (true) { @@ -305,8 +281,8 @@ class ArrayTernaryTrie extends AbstractTrie { char c = s.charAt(offset++); len--; - if (isCaseInsensitive() && c < 128) - c = StringUtil.lowercases[c]; + if (isCaseInsensitive()) + c = StringUtil.asciiToLowerCase(c); while (true) { @@ -363,7 +339,7 @@ class ArrayTernaryTrie extends AbstractTrie byte c = (byte)(b[offset++] & 0x7f); len--; if (isCaseInsensitive()) - c = (byte)StringUtil.lowercases[c]; + c = StringUtil.asciiToLowerCase(c); while (true) { @@ -406,7 +382,7 @@ class ArrayTernaryTrie extends AbstractTrie { byte c = (byte)(b.get(o + i) & 0x7f); if (isCaseInsensitive()) - c = (byte)StringUtil.lowercases[c]; + c = StringUtil.asciiToLowerCase(c); while (true) { @@ -443,20 +419,20 @@ class ArrayTernaryTrie extends AbstractTrie public String toString() { StringBuilder buf = new StringBuilder(); + buf.append("ATT@").append(Integer.toHexString(hashCode())).append('{'); + buf.append("ci=").append(isCaseInsensitive()).append(';'); + buf.append("c=").append(_tree.length / ROW_SIZE).append(';'); for (int r = 0; r <= _rows; r++) { if (_key[r] != null && _value[r] != null) { - buf.append(','); + if (r != 0) + buf.append(','); buf.append(_key[r]); buf.append('='); - buf.append(_value[r].toString()); + buf.append(String.valueOf(_value[r])); } } - if (buf.length() == 0) - return "{}"; - - buf.setCharAt(0, '{'); buf.append('}'); return buf.toString(); } @@ -529,12 +505,13 @@ class ArrayTernaryTrie extends AbstractTrie } } - static class Growing extends AbstractTrie + @Deprecated + public static class Growing extends AbstractTrie { private final int _growby; private ArrayTernaryTrie _trie; - Growing(boolean insensitive, int capacity, int growby) + public Growing(boolean insensitive, int capacity, int growby) { super(insensitive); _growby = growby; diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTrie.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTrie.java index 643a00af579..727dddf6287 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTrie.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayTrie.java @@ -13,12 +13,12 @@ package org.eclipse.jetty.util; -import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; /** *

    A Trie String lookup data structure using a fixed size array.

    @@ -29,8 +29,8 @@ import java.util.Set; * indexed in each lookup table, whilst infrequently used characters * must use a big character table. *

    - *

    This Trie is very space efficient if the key characters are - * from ' ', '+', '-', ':', ';', '.', 'A' to 'Z' or 'a' to 'z'. + *

    This Trie is space efficient if the key characters are + * from ' ', '+', '-', ':', ';', '.', '0' - '9', A' to 'Z' or 'a' to 'z' * Other ISO-8859-1 characters can be used by the key, but less space * efficiently. *

    @@ -45,221 +45,331 @@ import java.util.Set; */ class ArrayTrie extends AbstractTrie { + public static int MAX_CAPACITY = Character.MAX_VALUE; /** * The Size of a Trie row is how many characters can be looked * up directly without going to a big index. This is set at * 32 to cover case insensitive alphabet and a few other common * characters. */ - private static final int ROW_SIZE = 32; + private static final int ROW_SIZE = 48; + private static final int BIG_ROW_INSENSITIVE = 22; + private static final int BIG_ROW_SENSITIVE = 48; + private static final int X = Integer.MIN_VALUE; /** * The index lookup table, this maps a character as a byte - * (ISO-8859-1 or UTF8) to an index within a Trie row + * (ISO-8859-1 or UTF8) to a Trie index within a Trie row. + * Positive values are column indexes within the main {@link #_table}. + * Negative values are indexes within a {@link Node#_bigRow}. + * Values of {@link #X} are not indexed and must be searched for + * in the extended {@link Node#_bigRow} */ - private static final int[] LOOKUP = + private static final int[] LOOKUP_INSENSITIVE = { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F - /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1, - /*3*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 28, 29, -1, -1, -1, -1, - /*4*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - /*5*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, - /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1 + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + /*0*/ X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, + /*1*/ X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, + /*2*/ -1, -2, -3, -4, -5, -6, -7, -8, -9,-10,-11, 43, 44, 45, 46, 47, + /*3*/ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 37, 38, 39, 40, 41, 42, + /*4*/-12, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + /*5*/ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,-13,-14,-15,-16, 36, + /*6*/-17, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + /*7*/ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,-18,-19,-20,-21, X, }; + /** + * The index lookup table, this maps a character as a byte + * (ISO-8859-1 or UTF8) to a Trie index within a Trie row. + * Positive values are column indexes within the main {@link #_table}. + * Negative values are indexes within a {@link Node#_bigRow}. + * Values of {@link #X} are not indexed and must be searched for + * in the extended {@link Node#_bigRow} + */ + private static final int[] LOOKUP_SENSITIVE = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + /*0*/ X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, + /*1*/ X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, + /*2*/ -1, -2, -3, -4, -5, -6, -7, -8, -9,-10,-11, 43, 44, 45, 46, 47, + /*3*/ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 37, 38, 39, 40, 41, 42, + /*4*/-12,-22,-23,-24,-25,-26,-27,-28,-29,-30,-31,-32,-33,-34,-35,-36, + /*5*/-37,-38,-39,-40,-41,-42,-43,-44,-45,-46,-47,-13,-14,-15,-16, 36, + /*6*/-17, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + /*7*/ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,-18,-19,-20,-21, X, + }; + + /** + * A Node in the tree. + * A Node instance is only needed for rows in the {@link #_table} array + * that either have a key/value pair or a {@link #_bigRow} extended row. + * @param The value type of the node. + */ + private static class Node + { + String _key; + V _value; + + /** + * A big row of indexes in which extended characters can be found. + * The first {@link ArrayTrie#_bigRowSize} entries are accessed by negative + * indexes from the {@link ArrayTrie#_lookup} table. The following entries + * are character/row pairs that must be searched looking for a match. + * A big row is dynamically allocated to minimum size required for it's entries. + */ + char[] _bigRow; + + @Override + public String toString() + { + return _key + "=" + _value; + } + } + /** * The Trie rows in a single array which allows a lookup of row,character * to the next row in the Trie. This is actually a 2 dimensional * array that has been flattened to achieve locality of reference. * The first ROW_SIZE entries are for row 0, then next ROW_SIZE * entries are for row 1 etc. So in general instead of using - * _rows[row][index], we use _rows[row*ROW_SIZE+index] to look up + * _rows[row][column], we use _rows[row*ROW_SIZE+column] to look up * the next row for a given character. * * The array is of characters rather than integers to save space. */ - private final char[] _rowIndex; - - /** - * The key (if any) for a Trie row. - * A row may be a leaf, a node or both in the Trie tree. - */ - private final String[] _key; - - /** - * The value (if any) for a Trie row. - * A row may be a leaf, a node or both in the Trie tree. - */ - private final V[] _value; - - /** - * A big index for each row. - * If a character outside of the lookup map is needed, - * then a big index will be created for the row, with - * 256 entries, one for each possible byte. - */ - private char[][] _bigIndex; - - /** - * The number of rows allocated - */ + private final char[] _table; + private final int[] _lookup; + private final Node[] _node; + private final int _bigRowSize; private char _rows; + /** Create a trie from capacity and content + * @param capacity The maximum capacity of the Trie or -1 for unlimited capacity + * @param caseSensitive True if the Trie keys are case sensitive + * @param contents The known contents of the Trie + * @param The value type of the Trie + * @return a Trie containing the contents or null if not possible. + */ + public static ArrayTrie from(int capacity, boolean caseSensitive, Map contents) + { + // can't do infinite capacity + if (capacity < 0) + return null; + + if (capacity > MAX_CAPACITY) + return null; + + ArrayTrie trie = new ArrayTrie<>(caseSensitive, capacity); + if (contents != null && !trie.putAll(contents)) + return null; + return trie; + } + /** * @param capacity The capacity of the trie, which at the worst case - * is the total number of characters of all keys stored in the Trie. - * The capacity needed is dependent of the shared prefixes of the keys. - * For example, a capacity of 6 nodes is required to store keys "foo" - * and "bar", but a capacity of only 4 is required to - * store "bar" and "bat". + * is the total number of characters of all keys stored in the Trie, + * plus 1 for the empty key. + * @see AbstractTrie#requiredCapacity(Set, boolean) */ - @SuppressWarnings("unchecked") ArrayTrie(int capacity) { - super(true); - capacity++; - _value = (V[])new Object[capacity]; - _rowIndex = new char[capacity * ROW_SIZE]; - _key = new String[capacity]; + this(false, capacity); } @SuppressWarnings("unchecked") - ArrayTrie(Map initialValues) + ArrayTrie(boolean caseSensitive, int capacity) { - super(true); - // The calculated requiredCapacity does not take into account the - // extra reserved slot for the empty string key, so we have to add 1. - int capacity = requiredCapacity(initialValues.keySet(), false) + 1; - _value = (V[])new Object[capacity]; - _rowIndex = new char[capacity * ROW_SIZE]; - _key = new String[capacity]; - for (Map.Entry entry : initialValues.entrySet()) - { - if (!put(entry.getKey(), entry.getValue())) - throw new AssertionError("Invalid capacity calculated (" + capacity + ") at '" + entry + "' for " + initialValues); - } + super(caseSensitive); + _bigRowSize = caseSensitive ? BIG_ROW_SENSITIVE : BIG_ROW_INSENSITIVE; + if (capacity > MAX_CAPACITY) + throw new IllegalArgumentException("Capacity " + capacity + " > " + MAX_CAPACITY); + _lookup = !caseSensitive ? LOOKUP_INSENSITIVE : LOOKUP_SENSITIVE; + _table = new char[capacity * ROW_SIZE]; + _node = new Node[capacity]; } @Override public void clear() { _rows = 0; - Arrays.fill(_value, null); - Arrays.fill(_rowIndex, (char)0); - Arrays.fill(_key, null); + Arrays.fill(_table, (char)0); + Arrays.fill(_node, null); } @Override - public boolean put(String s, V v) + public boolean put(String key, V value) { - int t = 0; - int k; - int limit = s.length(); - for (k = 0; k < limit; k++) + int row = 0; + int limit = key.length(); + for (int i = 0; i < limit; i++) { - char c = s.charAt(k); - - int index = LOOKUP[c & 0x7f]; - if (index >= 0) + char c = key.charAt(i); + int column = c > 0x7f ? Integer.MIN_VALUE : _lookup[c]; + if (column >= 0) { - int idx = t * ROW_SIZE + index; - t = _rowIndex[idx]; - if (t == 0) + // This character is indexed to a column of the main table + int idx = row * ROW_SIZE + column; + row = _table[idx]; + if (row == 0) { - if (++_rows >= _value.length) + // not found so we need a new row + if (_rows == _node.length - 1) return false; - t = _rowIndex[idx] = _rows; + row = _table[idx] = ++_rows; + } + } + else if (column != Integer.MIN_VALUE) + { + // This character is indexed to a column in the nodes bigRow + int idx = -column; + Node node = _node[row]; + if (node == null) + node = _node[row] = new Node<>(); + char[] big = node._bigRow; + row = (big == null || idx >= big.length) ? 0 : big[idx]; + + if (row == 0) + { + // Not found, we need a new row + if (_rows == _node.length - 1) + return false; + + // Expand the size of the bigRow to have +1 extended lookups + if (big == null) + big = node._bigRow = new char[idx + 1]; + else if (idx >= big.length) + big = node._bigRow = Arrays.copyOf(big, idx + 1); + + row = big[idx] = ++_rows; } } - else if (c > 127) - throw new IllegalArgumentException("non ascii character"); else { - if (_bigIndex == null) - _bigIndex = new char[_value.length][]; - if (t >= _bigIndex.length) - return false; - char[] big = _bigIndex[t]; - if (big == null) - big = _bigIndex[t] = new char[128]; - t = big[c]; - if (t == 0) + // This char is neither in the normal table, nor the first part of a bigRow + // Look for it linearly in an extended big row. + int last = row; + row = 0; + Node node = _node[last]; + if (node != null) { - if (_rows == _value.length) + char[] big = node._bigRow; + if (big != null) + { + for (int idx = _bigRowSize; idx < big.length; idx += 2) + { + if (big[idx] == c) + { + row = big[idx + 1]; + break; + } + } + } + } + + if (row == 0) + { + // Not found, so we need a new row + if (_rows == _node.length - 1) return false; - t = big[c] = ++_rows; + + if (node == null) + node = _node[last] = new Node<>(); + char[] big = node._bigRow; + + // Expand the size of the bigRow to have extended lookups + if (big == null) + big = node._bigRow = new char[_bigRowSize + 2]; + else + big = node._bigRow = Arrays.copyOf(big, Math.max(big.length, _bigRowSize) + 2); + + // set the lookup char and its row + // TODO if the extended big row entries were sorted, then missed lookups could be aborted sooner + // TODO and/or a binary chop search could be done for hits. + big[big.length - 2] = c; + row = big[big.length - 1] = ++_rows; } } } - if (t >= _key.length) + // We have processed all characters so set the key and value in the current Node + Node node = _node[row]; + if (node == null) + node = _node[row] = new Node<>(); + node._key = key; + node._value = value; + return true; + } + + private int lookup(int row, char c) + { + // If the char is small we can lookup in the index table + if (c < 0x80) { - _rows = (char)_key.length; - return false; + int column = _lookup[c]; + if (column != Integer.MIN_VALUE) + { + // The char is indexed, so should be in normal row or bigRow + if (column >= 0) + { + // look in the normal row + int idx = row * ROW_SIZE + column; + row = _table[idx]; + } + else + { + // Look in the indexed part of the bigRow + Node node = _node[row]; + char[] big = node == null ? null : _node[row]._bigRow; + int idx = -column; + if (big == null || idx >= big.length) + return -1; + row = big[idx]; + } + return row == 0 ? -1 : row; + } } - _key[t] = v == null ? null : s; - _value[t] = v; - return true; + // Not an indexed char, so do a linear search through he tail of the bigRow + Node node = _node[row]; + char[] big = node == null ? null : node._bigRow; + if (big != null) + { + for (int i = _bigRowSize; i < big.length; i += 2) + if (big[i] == c) + return big[i + 1]; + } + + return -1; } @Override public V get(String s, int offset, int len) { - int t = 0; + int row = 0; for (int i = 0; i < len; i++) { char c = s.charAt(offset + i); - int index = LOOKUP[c & 0x7f]; - if (index >= 0) - { - int idx = t * ROW_SIZE + index; - t = _rowIndex[idx]; - if (t == 0) - return null; - } - else - { - char[] big = _bigIndex == null ? null : _bigIndex[t]; - if (big == null) - return null; - t = big[c]; - if (t == 0) - return null; - } + row = lookup(row, c); + if (row < 0) + return null; } - return _value[t]; + Node node = _node[row]; + return node == null ? null : node._value; } @Override public V get(ByteBuffer b, int offset, int len) { - int t = 0; + int row = 0; for (int i = 0; i < len; i++) { byte c = b.get(offset + i); - int index = LOOKUP[c & 0x7f]; - if (index >= 0) - { - int idx = t * ROW_SIZE + index; - t = _rowIndex[idx]; - if (t == 0) - return null; - } - else - { - char[] big = _bigIndex == null ? null : _bigIndex[t]; - if (big == null) - return null; - t = big[c]; - if (t == 0) - return null; - } + row = lookup(row, (char)(c & 0xff)); + if (row < 0) + return null; } - return (V)_value[t]; + Node node = _node[row]; + return node == null ? null : node._value; } @Override @@ -282,177 +392,108 @@ class ArrayTrie extends AbstractTrie return getBest(0, s, offset, len); } - private V getBest(int t, String s, int offset, int len) + private V getBest(int row, String s, int offset, int len) { int pos = offset; for (int i = 0; i < len; i++) { char c = s.charAt(pos++); - int index = LOOKUP[c & 0x7f]; - if (index >= 0) - { - int idx = t * ROW_SIZE + index; - int nt = _rowIndex[idx]; - if (nt == 0) - break; - t = nt; - } - else - { - char[] big = _bigIndex == null ? null : _bigIndex[t]; - if (big == null) - return null; - int nt = big[c]; - if (nt == 0) - break; - t = nt; - } + int next = lookup(row, c); + if (next < 0) + break; - // Is the next Trie is a match - if (_key[t] != null) + // Is the row a match? + Node node = _node[row]; + if (node != null && node._key != null) { // Recurse so we can remember this possibility - V best = getBest(t, s, offset + i + 1, len - i - 1); + V best = getBest(next, s, offset + i + 1, len - i - 1); if (best != null) return best; - return (V)_value[t]; + return node._value; } + + row = next; } - return (V)_value[t]; + Node node = _node[row]; + return node == null ? null : node._value; } - private V getBest(int t, byte[] b, int offset, int len) + private V getBest(int row, byte[] b, int offset, int len) { for (int i = 0; i < len; i++) { byte c = b[offset + i]; - int index = LOOKUP[c & 0x7f]; - if (index >= 0) - { - int idx = t * ROW_SIZE + index; - int nt = _rowIndex[idx]; - if (nt == 0) - break; - t = nt; - } - else - { - char[] big = _bigIndex == null ? null : _bigIndex[t]; - if (big == null) - return null; - int nt = big[c]; - if (nt == 0) - break; - t = nt; - } + int next = lookup(row, (char)(c & 0xff)); + if (next < 0) + break; - // Is the next Trie is a match - if (_key[t] != null) + // Is the next row a match? + Node node = _node[row]; + if (node != null && node._key != null) { // Recurse so we can remember this possibility - V best = getBest(t, b, offset + i + 1, len - i - 1); + V best = getBest(next, b, offset + i + 1, len - i - 1); if (best != null) return best; - break; + return node._value; } + + row = next; } - return (V)_value[t]; + Node node = _node[row]; + return node == null ? null : node._value; } - private V getBest(int t, ByteBuffer b, int offset, int len) + private V getBest(int row, ByteBuffer b, int offset, int len) { int pos = b.position() + offset; for (int i = 0; i < len; i++) { byte c = b.get(pos++); - int index = LOOKUP[c & 0x7f]; - if (index >= 0) - { - int idx = t * ROW_SIZE + index; - int nt = _rowIndex[idx]; - if (nt == 0) - break; - t = nt; - } - else - { - char[] big = _bigIndex == null ? null : _bigIndex[t]; - if (big == null) - return null; - int nt = big[c]; - if (nt == 0) - break; - t = nt; - } + int next = lookup(row, (char)(c & 0xff)); + if (next < 0) + break; - // Is the next Trie is a match - if (_key[t] != null) + // Is the next row a match? + Node node = _node[row]; + if (node != null && node._key != null) { // Recurse so we can remember this possibility - V best = getBest(t, b, offset + i + 1, len - i - 1); + V best = getBest(next, b, offset + i + 1, len - i - 1); if (best != null) return best; - break; + return node._value; } + + row = next; } - return (V)_value[t]; + Node node = _node[row]; + return node == null ? null : node._value; } @Override public String toString() { - StringBuilder buf = new StringBuilder(); - toString(buf, 0); - - if (buf.length() == 0) - return "{}"; - - buf.setCharAt(0, '{'); - buf.append('}'); - return buf.toString(); - } - - private void toString(Appendable out, int t) - { - if (_value[t] != null) - { - try - { - out.append(','); - out.append(_key[t]); - out.append('='); - out.append(_value[t].toString()); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - for (int i = 0; i < ROW_SIZE; i++) - { - int idx = t * ROW_SIZE + i; - if (_rowIndex[idx] != 0) - toString(out, _rowIndex[idx]); - } - - char[] big = _bigIndex == null ? null : _bigIndex[t]; - if (big != null) - { - for (int i : big) - { - if (i != 0) - toString(out, i); - } - } + return + "AT@" + Integer.toHexString(hashCode()) + '{' + + "cs=" + isCaseSensitive() + ';' + + "c=" + _table.length / ROW_SIZE + ';' + + Arrays.stream(_node) + .filter(n -> n != null && n._key != null) + .map(Node::toString) + .collect(Collectors.joining(",")) + + '}'; } @Override public Set keySet() { - Set keys = new HashSet<>(); - keySet(keys, 0); - return keys; + return Arrays.stream(_node) + .filter(Objects::nonNull) + .map(n -> n._key) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); } @Override @@ -467,26 +508,72 @@ class ArrayTrie extends AbstractTrie return keySet().isEmpty(); } - private void keySet(Set set, int t) + public void dumpStdErr() { - if (t < _value.length && _value[t] != null) - set.add(_key[t]); - - for (int i = 0; i < ROW_SIZE; i++) + System.err.print("row:"); + for (int c = 0; c < ROW_SIZE; c++) { - int idx = t * ROW_SIZE + i; - if (idx < _rowIndex.length && _rowIndex[idx] != 0) - keySet(set, _rowIndex[idx]); - } - - char[] big = _bigIndex == null || t >= _bigIndex.length ? null : _bigIndex[t]; - if (big != null) - { - for (int i : big) + for (int i = 0; i < 0x7f; i++) { - if (i != 0) - keySet(set, i); + if (_lookup[i] == c) + { + System.err.printf(" %s", (char)i); + break; + } } } + System.err.println(); + System.err.print("big:"); + for (int c = 0; c < _bigRowSize; c++) + { + for (int i = 0; i < 0x7f; i++) + { + if (-_lookup[i] == c) + { + System.err.printf(" %s", (char)i); + break; + } + } + } + System.err.println(); + + for (int row = 0; row <= _rows; row++) + { + System.err.printf("%3x:", row); + for (int c = 0; c < ROW_SIZE; c++) + { + char ch = _table[row * ROW_SIZE + c]; + if (ch == 0) + System.err.print(" ."); + else + System.err.printf("%3x", (int)ch); + } + Node node = _node[row]; + if (node != null) + { + System.err.printf(" : %s%n", node); + char[] bigRow = node._bigRow; + if (bigRow != null) + { + System.err.print(" :"); + for (int c = 0; c < Math.min(_bigRowSize, bigRow.length); c++) + { + char ch = bigRow[c]; + if (ch == 0) + System.err.print(" _"); + else + System.err.printf("%3x", (int)ch); + } + + for (int c = _bigRowSize; c < bigRow.length; c += 2) + System.err.printf(" %s>%x", bigRow[c], (int)bigRow[c + 1]); + + System.err.println(); + } + } + else + System.err.println(); + } + System.err.println(); } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/EmptyTrie.java b/jetty-util/src/main/java/org/eclipse/jetty/util/EmptyTrie.java index 65b1aab204e..0ccfaeb0e34 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/EmptyTrie.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/EmptyTrie.java @@ -15,18 +15,26 @@ package org.eclipse.jetty.util; import java.nio.ByteBuffer; import java.util.Collections; +import java.util.Map; +import java.util.Objects; import java.util.Set; /** * An empty trie implementation that never contains anything and never accepts new entries. + * * @param the entry type */ class EmptyTrie extends AbstractTrie { @SuppressWarnings("rawtypes") - private static final EmptyTrie SENSITIVE = new EmptyTrie<>(false); + private static final EmptyTrie SENSITIVE = new EmptyTrie<>(true); @SuppressWarnings("rawtypes") - private static final EmptyTrie INSENSITIVE = new EmptyTrie<>(true); + private static final EmptyTrie INSENSITIVE = new EmptyTrie<>(false); + + private EmptyTrie(boolean caseSensitive) + { + super(caseSensitive); + } @SuppressWarnings("unchecked") public static EmptyTrie instance(boolean caseSensitive) @@ -34,15 +42,21 @@ class EmptyTrie extends AbstractTrie return caseSensitive ? SENSITIVE : INSENSITIVE; } - private EmptyTrie(boolean insensitive) + @Override + public void clear() { - super(insensitive); } @Override - public boolean put(String s, V v) + public V get(String s) { - return false; + return null; + } + + @Override + public V get(ByteBuffer b) + { + return null; } @Override @@ -57,6 +71,30 @@ class EmptyTrie extends AbstractTrie return null; } + @Override + public V getBest(String s) + { + return null; + } + + @Override + public V getBest(byte[] b, int offset, int len) + { + return null; + } + + @Override + public V getBest(ByteBuffer b) + { + return null; + } + + @Override + public V getBest(byte[] b) + { + return null; + } + @Override public V getBest(String s, int offset, int len) { @@ -81,6 +119,20 @@ class EmptyTrie extends AbstractTrie return Collections.emptySet(); } + @Override + public boolean put(V v) + { + Objects.requireNonNull(v); + return false; + } + + @Override + public boolean put(String s, V v) + { + Objects.requireNonNull(s); + return false; + } + @Override public int size() { @@ -88,7 +140,8 @@ class EmptyTrie extends AbstractTrie } @Override - public void clear() + protected boolean putAll(Map contents) { + return false; } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Index.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Index.java index 3a9e02da83a..5a12e7777b9 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/Index.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Index.java @@ -35,7 +35,7 @@ public interface Index V get(String s); /** - * Get an exact match from a segment of a ByteBuufer as key + * Get an exact match from a segment of a ByteBuffer as key * * @param b The buffer * @return The value or null if not found @@ -53,7 +53,7 @@ public interface Index V get(String s, int offset, int len); /** - * Get an exact match from a segment of a ByteBuufer as key + * Get an exact match from a segment of a ByteBuffer as key * * @param b The buffer * @param offset The offset within the buffer of the key @@ -72,6 +72,14 @@ public interface Index */ V getBest(String s, int offset, int len); + /** + * Get the best match from key in a String. + * + * @param s The string + * @return The value or null if not found + */ + V getBest(String s); + /** * Get the best match from key in a byte buffer. * The key is assumed to by ISO_8859_1 characters. @@ -84,12 +92,16 @@ public interface Index V getBest(ByteBuffer b, int offset, int len); /** - * Get the best match from key in a String. + * Get the best match from key in a byte buffer. + * The key is assumed to by ISO_8859_1 characters. * - * @param s The string + * @param b The buffer * @return The value or null if not found */ - V getBest(String s); + default V getBest(ByteBuffer b) + { + return getBest(b, 0, b.remaining()); + } /** * Get the best match from key in a byte array. @@ -102,6 +114,18 @@ public interface Index */ V getBest(byte[] b, int offset, int len); + /** + * Get the best match from key in a byte array. + * The key is assumed to by ISO_8859_1 characters. + * + * @param b The buffer + * @return The value or null if not found + */ + default V getBest(byte[] b) + { + return getBest(b, 0, b.length); + } + /** * Check if the index contains any entry. * @@ -133,8 +157,8 @@ public interface Index /** * Put an entry into the index. * - * @param s The key for the entry - * @param v The value of the entry + * @param s The key for the entry. Must be non null, but can be empty. + * @param v The value of the entry. Must be non null. * @return True if the index had capacity to add the field. */ boolean put(String s, V v); @@ -188,46 +212,61 @@ public interface Index return this; } + /** + * Configure the index to be mutable. + * + * @return a {@link Mutable.Builder} configured like this builder. + */ + public Mutable.Builder mutable() + { + return this; + } + /** * Build a {@link Mutable} instance. * @return a {@link Mutable} instance. */ public Mutable build() { - if (contents != null && maxCapacity == 0) - throw new IllegalStateException("Cannot create a mutable index with maxCapacity=0 and some contents"); + if (maxCapacity == 0) + return EmptyTrie.instance(caseSensitive); - // TODO we need to consider large size and alphabet when picking a trie impl - Mutable result; - if (maxCapacity > 0) - { - result = new ArrayTernaryTrie<>(!caseSensitive, maxCapacity); - } - else if (maxCapacity < 0) - { - if (caseSensitive) - result = new ArrayTernaryTrie.Growing<>(false, 512, 512); - else - result = new TreeTrie<>(); - } - else - { - result = EmptyTrie.instance(caseSensitive); - } + // Work out needed capacity + int capacity = (contents == null) ? 0 : AbstractTrie.requiredCapacity(contents.keySet(), caseSensitive); - if (contents != null) - { - for (Map.Entry entry : contents.entrySet()) - { - if (!result.put(entry.getKey(), entry.getValue())) - throw new AssertionError("Index capacity exceeded at " + entry.getKey()); - } - } - return result; + // check capacities + if (maxCapacity >= 0 && capacity > maxCapacity) + throw new IllegalStateException("Insufficient maxCapacity for contents"); + + // try all the tries + AbstractTrie trie = ArrayTrie.from(maxCapacity, caseSensitive, contents); + if (trie != null) + return trie; + trie = TreeTrie.from(caseSensitive, contents); + if (trie != null) + return trie; + + // Nothing suitable + throw new IllegalStateException("No suitable Trie implementation: " + this); } } } + /** + * A special purpose static builder for fast creation of specific Index type + * @param maxCapacity The max capacity of the index + * @param The type of the index + * @return A case sensitive mutable Index tacking visible ASCII alphabet to a max capacity. + */ + static Mutable buildCaseSensitiveMutableVisibleAsciiAlphabet(int maxCapacity) + { + if (maxCapacity < 0 || maxCapacity > ArrayTrie.MAX_CAPACITY) + return new TreeTrie<>(true); + if (maxCapacity == 0) + return EmptyTrie.instance(true); + return new ArrayTrie<>(true, maxCapacity); + } + /** * Builder of {@link Index} instances. * @param the entry type @@ -242,7 +281,8 @@ public interface Index */ public Builder() { - this(false, null); + this.caseSensitive = false; + this.contents = null; } Builder(boolean caseSensitive, Map contents) @@ -349,11 +389,22 @@ public interface Index if (contents == null) return EmptyTrie.instance(caseSensitive); - // TODO we need to consider large size and alphabet when picking a trie impl - if (caseSensitive) - return new ArrayTernaryTrie<>(false, contents); - else - return new ArrayTrie<>(contents); + int capacity = AbstractTrie.requiredCapacity(contents.keySet(), caseSensitive); + + AbstractTrie trie = ArrayTrie.from(capacity, caseSensitive, contents); + if (trie != null) + return trie; + trie = TreeTrie.from(caseSensitive, contents); + if (trie != null) + return trie; + + throw new IllegalStateException("No suitable Trie implementation : " + this); + } + + @Override + public String toString() + { + return String.format("%s{c=%d,cs=%b}", super.toString(), contents == null ? 0 : contents.size(), caseSensitive); } } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index 148c30b4805..2b6777dc3b9 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -75,8 +75,7 @@ public class StringUtil } // @checkstyle-disable-check : IllegalTokenTextCheck - - public static final char[] lowercases = + private static final char[] LOWERCASES = { '\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007', '\010', '\011', '\012', '\013', '\014', '\015', '\016', '\017', @@ -96,8 +95,73 @@ public class StringUtil '\170', '\171', '\172', '\173', '\174', '\175', '\176', '\177' }; + // @checkstyle-disable-check : IllegalTokenTextCheck + private static final char[] UPPERCASES = + { + '\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007', + '\010', '\011', '\012', '\013', '\014', '\015', '\016', '\017', + '\020', '\021', '\022', '\023', '\024', '\025', '\026', '\027', + '\030', '\031', '\032', '\033', '\034', '\035', '\036', '\037', + '\040', '\041', '\042', '\043', '\044', '\045', '\046', '\047', + '\050', '\051', '\052', '\053', '\054', '\055', '\056', '\057', + '\060', '\061', '\062', '\063', '\064', '\065', '\066', '\067', + '\070', '\071', '\072', '\073', '\074', '\075', '\076', '\077', + '\100', '\101', '\102', '\103', '\104', '\105', '\106', '\107', + '\110', '\111', '\112', '\113', '\114', '\115', '\116', '\117', + '\120', '\121', '\122', '\123', '\124', '\125', '\126', '\127', + '\130', '\131', '\132', '\133', '\134', '\135', '\136', '\137', + '\140', '\101', '\102', '\103', '\104', '\105', '\106', '\107', + '\110', '\111', '\112', '\113', '\114', '\115', '\116', '\117', + '\120', '\121', '\122', '\123', '\124', '\125', '\126', '\127', + '\130', '\131', '\132', '\173', '\174', '\175', '\176', '\177' + }; + // @checkstyle-enable-check : IllegalTokenTextCheck + /** + * fast lower case conversion. Only works on ascii (not unicode) + * + * @param c the char to convert + * @return a lower case version of c + */ + public static char asciiToLowerCase(char c) + { + return (c < 0x80) ? LOWERCASES[c] : c; + } + + /** + * fast lower case conversion. Only works on ascii (not unicode) + * + * @param c the byte to convert + * @return a lower case version of c + */ + public static byte asciiToLowerCase(byte c) + { + return (c > 0) ? (byte)LOWERCASES[c] : c; + } + + /** + * fast upper case conversion. Only works on ascii (not unicode) + * + * @param c the char to convert + * @return a upper case version of c + */ + public static char asciiToUpperCase(char c) + { + return (c < 0x80) ? UPPERCASES[c] : c; + } + + /** + * fast upper case conversion. Only works on ascii (not unicode) + * + * @param c the byte to convert + * @return a upper case version of c + */ + public static byte asciiToUpperCase(byte c) + { + return (c > 0) ? (byte)UPPERCASES[c] : c; + } + /** * fast lower case conversion. Only works on ascii (not unicode) * @@ -117,7 +181,7 @@ public class StringUtil char c1 = s.charAt(i); if (c1 <= 127) { - char c2 = lowercases[c1]; + char c2 = LOWERCASES[c1]; if (c1 != c2) { c = s.toCharArray(); @@ -129,12 +193,48 @@ public class StringUtil while (i-- > 0) { if (c[i] <= 127) - c[i] = lowercases[c[i]]; + c[i] = LOWERCASES[c[i]]; } return c == null ? s : new String(c); } + /** + * fast upper case conversion. Only works on ascii (not unicode) + * + * @param s the string to convert + * @return a lower case version of s + */ + public static String asciiToUpperCase(String s) + { + if (s == null) + return null; + + char[] c = null; + int i = s.length(); + // look for first conversion + while (i-- > 0) + { + char c1 = s.charAt(i); + if (c1 <= 127) + { + char c2 = UPPERCASES[c1]; + if (c1 != c2) + { + c = s.toCharArray(); + c[i] = c2; + break; + } + } + } + while (i-- > 0) + { + if (c[i] <= 127) + c[i] = UPPERCASES[c[i]]; + } + return c == null ? s : new String(c); + } + /** * Replace all characters from input string that are known to have * special meaning in various filesystems. @@ -199,9 +299,9 @@ public class StringUtil if (c1 != c2) { if (c1 <= 127) - c1 = lowercases[c1]; + c1 = LOWERCASES[c1]; if (c2 <= 127) - c2 = lowercases[c2]; + c2 = LOWERCASES[c2]; if (c1 != c2) return false; } @@ -229,9 +329,9 @@ public class StringUtil if (c1 != c2) { if (c1 <= 127) - c1 = lowercases[c1]; + c1 = LOWERCASES[c1]; if (c2 <= 127) - c2 = lowercases[c2]; + c2 = LOWERCASES[c2]; if (c1 != c2) return false; } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/TreeTrie.java b/jetty-util/src/main/java/org/eclipse/jetty/util/TreeTrie.java index abecb46ce1f..e443e67cdfe 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/TreeTrie.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/TreeTrie.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -39,9 +40,9 @@ import java.util.Set; */ class TreeTrie extends AbstractTrie { - private static final int[] LOOKUP = + private static final int[] LOOKUP_INSENSITIVE = { - // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // 0 1 2 3 4 5 6 7 8 9 A B C D E F /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1, @@ -51,57 +52,95 @@ class TreeTrie extends AbstractTrie /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1 }; + private static final int[] LOOKUP_SENSITIVE = + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + /*0*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*1*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*2*/31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26, -1, 27, 30, -1, + /*3*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 28, 29, -1, -1, -1, -1, + /*4*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*5*/-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /*6*/-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + /*7*/15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1 + }; private static final int INDEX = 32; - private final TreeTrie[] _nextIndex; - private final List> _nextOther = new ArrayList<>(); - private final char _c; - private String _key; - private V _value; + /** Create a trie from capacity and content + * @param caseSensitive True if the Trie keys are case sensitive + * @param contents The known contents of the Trie + * @param The value type of the Trie + * @return a Trie containing the contents or null if not possible. + */ + public static AbstractTrie from(boolean caseSensitive, Map contents) + { + TreeTrie trie = new TreeTrie<>(caseSensitive); + if (contents != null && !trie.putAll(contents)) + return null; + return trie; + } + + private static class Node + { + private final Node[] _nextIndex; + private final List> _nextOther = new ArrayList<>(); + private final char _c; + private String _key; + private V _value; + + // TODO made this use a variable lookup row like ArrayTrie + @SuppressWarnings("unchecked") + private Node(char c) + { + _nextIndex = new Node[INDEX]; + this._c = c; + } + } + + private final int[] _lookup; + private final Node _root; + @SuppressWarnings("unchecked") TreeTrie() { - super(true); - _nextIndex = new TreeTrie[INDEX]; - _c = 0; + this(false); } - - @SuppressWarnings("unchecked") - private TreeTrie(char c) + + TreeTrie(boolean caseSensitive) { - super(true); - _nextIndex = new TreeTrie[INDEX]; - this._c = c; + super(caseSensitive); + _lookup = caseSensitive ? LOOKUP_SENSITIVE : LOOKUP_INSENSITIVE; + _root = new Node((char)0); } - + @Override public void clear() { - Arrays.fill(_nextIndex, null); - _nextOther.clear(); - _key = null; - _value = null; + Arrays.fill(_root._nextIndex, null); + _root._nextOther.clear(); + _root._key = null; + _root._value = null; } @Override public boolean put(String s, V v) { - TreeTrie t = this; + Node t = _root; int limit = s.length(); for (int k = 0; k < limit; k++) { char c = s.charAt(k); - int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + int index = c < 0x7f ? _lookup[c] : -1; if (index >= 0) { if (t._nextIndex[index] == null) - t._nextIndex[index] = new TreeTrie(c); + t._nextIndex[index] = new Node(c); t = t._nextIndex[index]; } else { - TreeTrie n = null; + Node n = null; for (int i = t._nextOther.size(); i-- > 0; ) { n = t._nextOther.get(i); @@ -111,7 +150,7 @@ class TreeTrie extends AbstractTrie } if (n == null) { - n = new TreeTrie(c); + n = new Node(c); t._nextOther.add(n); } t = n; @@ -125,11 +164,11 @@ class TreeTrie extends AbstractTrie @Override public V get(String s, int offset, int len) { - TreeTrie t = this; + Node t = _root; for (int i = 0; i < len; i++) { char c = s.charAt(offset + i); - int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + int index = c < 0x7f ? _lookup[c] : -1; if (index >= 0) { if (t._nextIndex[index] == null) @@ -138,7 +177,7 @@ class TreeTrie extends AbstractTrie } else { - TreeTrie n = null; + Node n = null; for (int j = t._nextOther.size(); j-- > 0; ) { n = t._nextOther.get(j); @@ -157,11 +196,11 @@ class TreeTrie extends AbstractTrie @Override public V get(ByteBuffer b, int offset, int len) { - TreeTrie t = this; + Node t = _root; for (int i = 0; i < len; i++) { byte c = b.get(offset + i); - int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + int index = c >= 0 && c < 0x7f ? _lookup[c] : -1; if (index >= 0) { if (t._nextIndex[index] == null) @@ -170,7 +209,7 @@ class TreeTrie extends AbstractTrie } else { - TreeTrie n = null; + Node n = null; for (int j = t._nextOther.size(); j-- > 0; ) { n = t._nextOther.get(j); @@ -189,11 +228,15 @@ class TreeTrie extends AbstractTrie @Override public V getBest(byte[] b, int offset, int len) { - TreeTrie t = this; + return getBest(_root, b, offset, len); + } + + private V getBest(Node t, byte[] b, int offset, int len) + { for (int i = 0; i < len; i++) { byte c = b[offset + i]; - int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + int index = c >= 0 && c < 0x7f ? _lookup[c] : -1; if (index >= 0) { if (t._nextIndex[index] == null) @@ -202,7 +245,7 @@ class TreeTrie extends AbstractTrie } else { - TreeTrie n = null; + Node n = null; for (int j = t._nextOther.size(); j-- > 0; ) { n = t._nextOther.get(j); @@ -219,7 +262,7 @@ class TreeTrie extends AbstractTrie if (t._key != null) { // Recurse so we can remember this possibility - V best = t.getBest(b, offset + i + 1, len - i - 1); + V best = getBest(t, b, offset + i + 1, len - i - 1); if (best != null) return best; break; @@ -243,11 +286,15 @@ class TreeTrie extends AbstractTrie @Override public V getBest(String s, int offset, int len) { - TreeTrie t = this; + return getBest(_root, s, offset, len); + } + + private V getBest(Node t, String s, int offset, int len) + { for (int i = 0; i < len; i++) { - byte c = (byte)(0xff & s.charAt(offset + i)); - int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + char c = s.charAt(offset + i); + int index = c < 0x7f ? _lookup[c] : -1; if (index >= 0) { if (t._nextIndex[index] == null) @@ -256,7 +303,7 @@ class TreeTrie extends AbstractTrie } else { - TreeTrie n = null; + Node n = null; for (int j = t._nextOther.size(); j-- > 0; ) { n = t._nextOther.get(j); @@ -273,7 +320,7 @@ class TreeTrie extends AbstractTrie if (t._key != null) { // Recurse so we can remember this possibility - V best = t.getBest(s, offset + i + 1, len - i - 1); + V best = getBest(t, s, offset + i + 1, len - i - 1); if (best != null) return best; break; @@ -287,17 +334,16 @@ class TreeTrie extends AbstractTrie { if (b.hasArray()) return getBest(b.array(), b.arrayOffset() + b.position() + offset, len); - return getBestByteBuffer(b, offset, len); + return getBest(_root, b, offset, len); } - private V getBestByteBuffer(ByteBuffer b, int offset, int len) + private V getBest(Node t, ByteBuffer b, int offset, int len) { - TreeTrie t = this; int pos = b.position() + offset; for (int i = 0; i < len; i++) { byte c = b.get(pos++); - int index = c >= 0 && c < 0x7f ? LOOKUP[c] : -1; + int index = c >= 0 && c < 0x7f ? _lookup[c] : -1; if (index >= 0) { if (t._nextIndex[index] == null) @@ -306,7 +352,7 @@ class TreeTrie extends AbstractTrie } else { - TreeTrie n = null; + Node n = null; for (int j = t._nextOther.size(); j-- > 0; ) { n = t._nextOther.get(j); @@ -323,7 +369,7 @@ class TreeTrie extends AbstractTrie if (t._key != null) { // Recurse so we can remember this possibility - V best = t.getBest(b, offset + i + 1, len - i - 1); + V best = getBest(t, b, offset + i + 1, len - i - 1); if (best != null) return best; break; @@ -336,44 +382,63 @@ class TreeTrie extends AbstractTrie public String toString() { StringBuilder buf = new StringBuilder(); - toString(buf, this); - - if (buf.length() == 0) - return "{}"; - - buf.setCharAt(0, '{'); + buf.append("TT@").append(Integer.toHexString(hashCode())).append('{'); + buf.append("ci=").append(isCaseInsensitive()).append(';'); + toString(buf, _root, ""); buf.append('}'); return buf.toString(); } - private static void toString(Appendable out, TreeTrie t) + private static void toString(Appendable out, Node t, String separator) { - if (t != null) + loop: while (true) { - if (t._value != null) + if (t != null) { - try + if (t._value != null) { - out.append(','); - out.append(t._key); - out.append('='); - out.append(t._value.toString()); + try + { + out.append(separator); + separator = ","; + out.append(t._key); + out.append('='); + out.append(t._value.toString()); + } + catch (IOException e) + { + throw new RuntimeException(e); + } } - catch (IOException e) + + for (int i = 0; i < INDEX;) { - throw new RuntimeException(e); + Node n = t._nextIndex[i++]; + if (n != null) + { + // can we avoid tail recurse? + if (i == INDEX && t._nextOther.size() == 0) + { + t = n; + continue loop; + } + // recurse + toString(out, n, separator); + } + } + for (int i = t._nextOther.size(); i-- > 0; ) + { + // can we avoid tail recurse? + if (i == 0) + { + t = t._nextOther.get(i); + continue loop; + } + toString(out, t._nextOther.get(i), separator); } } - for (int i = 0; i < INDEX; i++) - { - if (t._nextIndex[i] != null) - toString(out, t._nextIndex[i]); - } - for (int i = t._nextOther.size(); i-- > 0; ) - { - toString(out, t._nextOther.get(i)); - } + break; } } @@ -381,11 +446,11 @@ class TreeTrie extends AbstractTrie public Set keySet() { Set keys = new HashSet<>(); - keySet(keys, this); + keySet(keys, _root); return keys; } - private static void keySet(Set set, TreeTrie t) + private static void keySet(Set set, Node t) { if (t != null) { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/IndexTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/IndexTest.java index a8ff8b3b5b5..c18bb1a746f 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/IndexTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/IndexTest.java @@ -15,46 +15,38 @@ package org.eclipse.jetty.util; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; public class IndexTest { @Test - public void belowMaxCapacityTest() + public void testImmutableTrieSelection() { - int size = 10_450; + // empty immutable index is always empty + assertThat(new Index.Builder().build(), instanceOf(EmptyTrie.class)); - Index.Builder builder = new Index.Builder<>(); - builder.caseSensitive(true); - for (int i = 0; i < size; i++) - { - builder.with("/test/group" + i, i); - } - Index index = builder.build(); + // index of ascii characters + assertThat(new Index.Builder().caseSensitive(false).with("name", "value").build(), instanceOf(ArrayTrie.class)); + assertThat(new Index.Builder().caseSensitive(true).with("name", "value").build(), instanceOf(ArrayTrie.class)); - for (int i = 0; i < size; i++) - { - Integer integer = index.get("/test/group" + i); - if (integer == null) - fail("missing entry for '/test/group" + i + "'"); - else if (integer != i) - fail("incorrect value for '/test/group" + i + "' (" + integer + ")"); - } + // large index + String hugekey = "x".repeat(Character.MAX_VALUE + 1); + assertTrue(new Index.Builder().caseSensitive(false).with(hugekey, "value").build() instanceof TreeTrie); + assertTrue(new Index.Builder().caseSensitive(true).with(hugekey, "value").build() instanceof TreeTrie); } @Test - public void overMaxCapacityTest() + public void testUnlimitdMutableTrieSelection() { - int size = 11_000; + assertThat(new Index.Builder().mutable().build(), instanceOf(TreeTrie.class)); + } - Index.Builder builder = new Index.Builder<>(); - builder.caseSensitive(true); - for (int i = 0; i < size; i++) - { - builder.with("/test/group" + i, i); - } - - assertThrows(IllegalArgumentException.class, builder::build); + @Test + public void testLimitedMutableTrieSelection() + { + assertThat(new Index.Builder().mutable().maxCapacity(500).build(), instanceOf(ArrayTrie.class)); + assertThat(new Index.Builder().mutable().maxCapacity(Character.MAX_VALUE + 1).build(), instanceOf(TreeTrie.class)); } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java index fa1ba2aa3b8..5474f5b4b43 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java @@ -13,12 +13,13 @@ package org.eclipse.jetty.util; -import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -26,32 +27,84 @@ import org.junit.jupiter.params.provider.MethodSource; import static org.eclipse.jetty.util.AbstractTrie.requiredCapacity; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.in; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck public class TrieTest { + private static final String[] KEYS = + { + "hello", + "helloHello", + "He", + "HELL", + "wibble", + "Wobble", + "foo-bar", + "foo+bar", + "HELL4" + }; + private static final String[] X_KEYS = Arrays.stream(KEYS).map(s -> "%" + s + "%").toArray(String[]::new); + private static final String[] NOT_KEYS = + { + "h", + "helloHell", + "helloHelloHELLO", + "wibble0", + "foo_bar", + "foo-bar-bob", + "HELL5", + "\u0000" + }; + + private static final String[] BEST_NOT_KEYS = + { + null, + "hello", + "helloHello", + "wibble", + null, + "foo-bar", + "HELL", + null, + }; + + private static final String[] BEST_NOT_KEYS_LOWER = + { + null, + "hello", + "hello", + "wibble", + null, + "foo-bar", + null, + null, + }; + + private static final String[] X_NOT_KEYS = Arrays.stream(NOT_KEYS).map(s -> "%" + s + "%%%%").toArray(String[]::new); + public static Stream implementations() { List> impls = new ArrayList<>(); - impls.add(new ArrayTrie(128)); - impls.add(new ArrayTernaryTrie(true, 128)); - impls.add(new ArrayTernaryTrie.Growing(true, 128, 128)); + for (boolean caseSensitive : new boolean[] {true, false}) + { + impls.add(new ArrayTrie(caseSensitive,128)); + impls.add(new ArrayTernaryTrie(caseSensitive, 128)); + impls.add(new TreeTrie<>(caseSensitive)); + } for (AbstractTrie trie : impls) { - trie.put("hello", 1); - trie.put("He", 2); - trie.put("HELL", 3); - trie.put("wibble", 4); - trie.put("Wobble", 5); - trie.put("foo-bar", 6); - trie.put("foo+bar", 7); - trie.put("HELL4", 8); - trie.put("", 9); + for (int i = 0; i < KEYS.length; i++) + { + if (!trie.put(KEYS[i], i)) + throw new IllegalStateException(); + } } return impls.stream().map(Arguments::of); @@ -61,159 +114,174 @@ public class TrieTest @MethodSource("implementations") public void testKeySet(AbstractTrie trie) throws Exception { - String[] values = new String[]{ - "hello", - "He", - "HELL", - "wibble", - "Wobble", - "foo-bar", - "foo+bar", - "HELL4", - "" - }; - - for (String value : values) - { - assertThat(value, is(in(trie.keySet()))); - } + for (String value : KEYS) + assertThat(value, trie.keySet().contains(value), is(true)); + for (String value : NOT_KEYS) + assertThat(value, trie.keySet().contains(value), is(false)); } @ParameterizedTest @MethodSource("implementations") public void testGetString(AbstractTrie trie) throws Exception { - assertEquals(1, trie.get("hello").intValue()); - assertEquals(2, trie.get("He").intValue()); - assertEquals(3, trie.get("HELL").intValue()); - assertEquals(4, trie.get("wibble").intValue()); - assertEquals(5, trie.get("Wobble").intValue()); - assertEquals(6, trie.get("foo-bar").intValue()); - assertEquals(7, trie.get("foo+bar").intValue()); - - assertEquals(1, trie.get("Hello").intValue()); - assertEquals(2, trie.get("HE").intValue()); - assertEquals(3, trie.get("heLL").intValue()); - assertEquals(4, trie.get("Wibble").intValue()); - assertEquals(5, trie.get("wobble").intValue()); - assertEquals(6, trie.get("Foo-bar").intValue()); - assertEquals(7, trie.get("FOO+bar").intValue()); - assertEquals(8, trie.get("HELL4").intValue()); - assertEquals(9, trie.get("").intValue()); - - assertEquals(null, trie.get("helloworld")); - assertEquals(null, trie.get("Help")); - assertEquals(null, trie.get("Blah")); + for (int i = 0; i < KEYS.length; i++) + assertThat(Integer.toString(i), trie.get(KEYS[i]), is(i)); + for (int i = 0; i < NOT_KEYS.length; i++) + assertThat(Integer.toString(i), trie.get(NOT_KEYS[i]), nullValue()); + for (int i = 0; i < KEYS.length; i++) + { + String k = KEYS[i].toLowerCase(); + Integer actual = trie.get(k); + if (k.equals(KEYS[i]) || trie.isCaseInsensitive()) + assertThat(k, actual, is(i)); + else + assertThat(k, actual, nullValue()); + } + for (int i = 0; i < KEYS.length; i++) + { + String k = KEYS[i].toUpperCase(); + Integer actual = trie.get(k); + if (k.equals(KEYS[i]) || trie.isCaseInsensitive()) + assertThat(k, actual, is(i)); + else + assertThat(k, actual, nullValue()); + } } @ParameterizedTest @MethodSource("implementations") public void testGetBuffer(AbstractTrie trie) throws Exception { - assertEquals(1, trie.get(BufferUtil.toBuffer("xhellox"), 1, 5).intValue()); - assertEquals(2, trie.get(BufferUtil.toBuffer("xhellox"), 1, 2).intValue()); - assertEquals(3, trie.get(BufferUtil.toBuffer("xhellox"), 1, 4).intValue()); - assertEquals(4, trie.get(BufferUtil.toBuffer("wibble"), 0, 6).intValue()); - assertEquals(5, trie.get(BufferUtil.toBuffer("xWobble"), 1, 6).intValue()); - assertEquals(6, trie.get(BufferUtil.toBuffer("xfoo-barx"), 1, 7).intValue()); - assertEquals(7, trie.get(BufferUtil.toBuffer("xfoo+barx"), 1, 7).intValue()); - - assertEquals(1, trie.get(BufferUtil.toBuffer("xhellox"), 1, 5).intValue()); - assertEquals(2, trie.get(BufferUtil.toBuffer("xHELLox"), 1, 2).intValue()); - assertEquals(3, trie.get(BufferUtil.toBuffer("xhellox"), 1, 4).intValue()); - assertEquals(4, trie.get(BufferUtil.toBuffer("Wibble"), 0, 6).intValue()); - assertEquals(5, trie.get(BufferUtil.toBuffer("xwobble"), 1, 6).intValue()); - assertEquals(6, trie.get(BufferUtil.toBuffer("xFOO-barx"), 1, 7).intValue()); - assertEquals(7, trie.get(BufferUtil.toBuffer("xFOO+barx"), 1, 7).intValue()); - - assertEquals(null, trie.get(BufferUtil.toBuffer("xHelloworldx"), 1, 10)); - assertEquals(null, trie.get(BufferUtil.toBuffer("xHelpx"), 1, 4)); - assertEquals(null, trie.get(BufferUtil.toBuffer("xBlahx"), 1, 4)); + for (int i = 0; i < KEYS.length; i++) + assertThat(Integer.toString(i), trie.get(BufferUtil.toBuffer(X_KEYS[i]), 1, KEYS[i].length()), is(i)); + for (int i = 0; i < NOT_KEYS.length; i++) + assertThat(Integer.toString(i), trie.get(BufferUtil.toBuffer(X_NOT_KEYS[i]), 1, NOT_KEYS[i].length()), nullValue()); + for (int i = 0; i < KEYS.length; i++) + { + String k = X_KEYS[i].toLowerCase(); + Integer actual = trie.get(BufferUtil.toBuffer(k), 1, KEYS[i].length()); + if (k.equals(X_KEYS[i]) || trie.isCaseInsensitive()) + assertThat(k, actual, is(i)); + else + assertThat(k, actual, nullValue()); + } + for (int i = 0; i < KEYS.length; i++) + { + String k = X_KEYS[i].toUpperCase(); + Integer actual = trie.get(BufferUtil.toBuffer(k), 1, KEYS[i].length()); + if (k.equals(X_KEYS[i]) || trie.isCaseInsensitive()) + assertThat(k, actual, is(i)); + else + assertThat(k, actual, nullValue()); + } } @ParameterizedTest @MethodSource("implementations") public void testGetDirectBuffer(AbstractTrie trie) throws Exception { - assertEquals(1, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 5).intValue()); - assertEquals(2, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 2).intValue()); - assertEquals(3, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 4).intValue()); - assertEquals(4, trie.get(BufferUtil.toDirectBuffer("wibble"), 0, 6).intValue()); - assertEquals(5, trie.get(BufferUtil.toDirectBuffer("xWobble"), 1, 6).intValue()); - assertEquals(6, trie.get(BufferUtil.toDirectBuffer("xfoo-barx"), 1, 7).intValue()); - assertEquals(7, trie.get(BufferUtil.toDirectBuffer("xfoo+barx"), 1, 7).intValue()); - - assertEquals(1, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 5).intValue()); - assertEquals(2, trie.get(BufferUtil.toDirectBuffer("xHELLox"), 1, 2).intValue()); - assertEquals(3, trie.get(BufferUtil.toDirectBuffer("xhellox"), 1, 4).intValue()); - assertEquals(4, trie.get(BufferUtil.toDirectBuffer("Wibble"), 0, 6).intValue()); - assertEquals(5, trie.get(BufferUtil.toDirectBuffer("xwobble"), 1, 6).intValue()); - assertEquals(6, trie.get(BufferUtil.toDirectBuffer("xFOO-barx"), 1, 7).intValue()); - assertEquals(7, trie.get(BufferUtil.toDirectBuffer("xFOO+barx"), 1, 7).intValue()); - - assertEquals(null, trie.get(BufferUtil.toDirectBuffer("xHelloworldx"), 1, 10)); - assertEquals(null, trie.get(BufferUtil.toDirectBuffer("xHelpx"), 1, 4)); - assertEquals(null, trie.get(BufferUtil.toDirectBuffer("xBlahx"), 1, 4)); + for (int i = 0; i < KEYS.length; i++) + assertThat(Integer.toString(i), trie.get(BufferUtil.toDirectBuffer(X_KEYS[i]), 1, KEYS[i].length()), is(i)); + for (int i = 0; i < NOT_KEYS.length; i++) + assertThat(Integer.toString(i), trie.get(BufferUtil.toDirectBuffer(X_NOT_KEYS[i]), 1, NOT_KEYS[i].length()), nullValue()); + for (int i = 0; i < KEYS.length; i++) + { + String k = X_KEYS[i].toLowerCase(); + Integer actual = trie.get(BufferUtil.toDirectBuffer(k), 1, KEYS[i].length()); + if (k.equals(X_KEYS[i]) || trie.isCaseInsensitive()) + assertThat(k, actual, is(i)); + else + assertThat(k, actual, nullValue()); + } + for (int i = 0; i < KEYS.length; i++) + { + String k = X_KEYS[i].toUpperCase(); + Integer actual = trie.get(BufferUtil.toDirectBuffer(k), 1, KEYS[i].length()); + if (k.equals(X_KEYS[i]) || trie.isCaseInsensitive()) + assertThat(k, actual, is(i)); + else + assertThat(k, actual, nullValue()); + } } @ParameterizedTest @MethodSource("implementations") public void testGetBestArray(AbstractTrie trie) throws Exception { - assertEquals(1, trie.getBest(StringUtil.getUtf8Bytes("xhelloxxxx"), 1, 8).intValue()); - assertEquals(2, trie.getBest(StringUtil.getUtf8Bytes("xhelxoxxxx"), 1, 8).intValue()); - assertEquals(3, trie.getBest(StringUtil.getUtf8Bytes("xhellxxxxx"), 1, 8).intValue()); - assertEquals(6, trie.getBest(StringUtil.getUtf8Bytes("xfoo-barxx"), 1, 8).intValue()); - assertEquals(8, trie.getBest(StringUtil.getUtf8Bytes("xhell4xxxx"), 1, 8).intValue()); + for (int i = 0; i < NOT_KEYS.length; i++) + { + String k = X_NOT_KEYS[i]; + Integer actual = trie.getBest(StringUtil.getUtf8Bytes(k), 1, X_NOT_KEYS[i].length() - 1); + Integer expected = BEST_NOT_KEYS[i] == null ? null : trie.get(BEST_NOT_KEYS[i]); + assertThat(k, actual, is(expected)); + } - assertEquals(1, trie.getBest(StringUtil.getUtf8Bytes("xHELLOxxxx"), 1, 8).intValue()); - assertEquals(2, trie.getBest(StringUtil.getUtf8Bytes("xHELxoxxxx"), 1, 8).intValue()); - assertEquals(3, trie.getBest(StringUtil.getUtf8Bytes("xHELLxxxxx"), 1, 8).intValue()); - assertEquals(6, trie.getBest(StringUtil.getUtf8Bytes("xfoo-BARxx"), 1, 8).intValue()); - assertEquals(8, trie.getBest(StringUtil.getUtf8Bytes("xHELL4xxxx"), 1, 8).intValue()); - assertEquals(9, trie.getBest(StringUtil.getUtf8Bytes("xZZZZZxxxx"), 1, 8).intValue()); + for (int i = 0; i < NOT_KEYS.length; i++) + { + String k = X_NOT_KEYS[i].toLowerCase(); + Integer actual = trie.getBest(StringUtil.getUtf8Bytes(k), 1, X_NOT_KEYS[i].length() - 1); + String[] expectations = trie.isCaseSensitive() ? BEST_NOT_KEYS_LOWER : BEST_NOT_KEYS; + Integer expected = expectations[i] == null ? null : trie.get(expectations[i]); + assertThat(k, actual, is(expected)); + } } @ParameterizedTest @MethodSource("implementations") public void testGetBestBuffer(AbstractTrie trie) throws Exception { - assertEquals(1, trie.getBest(BufferUtil.toBuffer("xhelloxxxx"), 1, 8).intValue()); - assertEquals(2, trie.getBest(BufferUtil.toBuffer("xhelxoxxxx"), 1, 8).intValue()); - assertEquals(3, trie.getBest(BufferUtil.toBuffer("xhellxxxxx"), 1, 8).intValue()); - assertEquals(6, trie.getBest(BufferUtil.toBuffer("xfoo-barxx"), 1, 8).intValue()); - assertEquals(8, trie.getBest(BufferUtil.toBuffer("xhell4xxxx"), 1, 8).intValue()); + for (int i = 0; i < NOT_KEYS.length; i++) + { + String k = X_NOT_KEYS[i]; + Integer actual = trie.getBest(BufferUtil.toBuffer(k), 1, X_NOT_KEYS[i].length() - 1); + Integer expected = BEST_NOT_KEYS[i] == null ? null : trie.get(BEST_NOT_KEYS[i]); + assertThat(k, actual, is(expected)); + } - assertEquals(1, trie.getBest(BufferUtil.toBuffer("xHELLOxxxx"), 1, 8).intValue()); - assertEquals(2, trie.getBest(BufferUtil.toBuffer("xHELxoxxxx"), 1, 8).intValue()); - assertEquals(3, trie.getBest(BufferUtil.toBuffer("xHELLxxxxx"), 1, 8).intValue()); - assertEquals(6, trie.getBest(BufferUtil.toBuffer("xfoo-BARxx"), 1, 8).intValue()); - assertEquals(8, trie.getBest(BufferUtil.toBuffer("xHELL4xxxx"), 1, 8).intValue()); - assertEquals(9, trie.getBest(BufferUtil.toBuffer("xZZZZZxxxx"), 1, 8).intValue()); - - ByteBuffer buffer = (ByteBuffer)BufferUtil.toBuffer("xhelloxxxxxxx").position(2); - assertEquals(1, trie.getBest(buffer, -1, 10).intValue()); + for (int i = 0; i < NOT_KEYS.length; i++) + { + String k = X_NOT_KEYS[i].toLowerCase(); + Integer actual = trie.getBest(BufferUtil.toBuffer(k), 1, X_NOT_KEYS[i].length() - 1); + String[] expectations = trie.isCaseSensitive() ? BEST_NOT_KEYS_LOWER : BEST_NOT_KEYS; + Integer expected = expectations[i] == null ? null : trie.get(expectations[i]); + assertThat(k, actual, is(expected)); + } } @ParameterizedTest @MethodSource("implementations") public void testGetBestDirectBuffer(AbstractTrie trie) throws Exception { - assertEquals(1, trie.getBest(BufferUtil.toDirectBuffer("xhelloxxxx"), 1, 8).intValue()); - assertEquals(2, trie.getBest(BufferUtil.toDirectBuffer("xhelxoxxxx"), 1, 8).intValue()); - assertEquals(3, trie.getBest(BufferUtil.toDirectBuffer("xhellxxxxx"), 1, 8).intValue()); - assertEquals(6, trie.getBest(BufferUtil.toDirectBuffer("xfoo-barxx"), 1, 8).intValue()); - assertEquals(8, trie.getBest(BufferUtil.toDirectBuffer("xhell4xxxx"), 1, 8).intValue()); + for (int i = 0; i < NOT_KEYS.length; i++) + { + String k = X_NOT_KEYS[i]; + Integer actual = trie.getBest(BufferUtil.toDirectBuffer(k), 1, X_NOT_KEYS[i].length() - 1); + Integer expected = BEST_NOT_KEYS[i] == null ? null : trie.get(BEST_NOT_KEYS[i]); + assertThat(k, actual, is(expected)); + } - assertEquals(1, trie.getBest(BufferUtil.toDirectBuffer("xHELLOxxxx"), 1, 8).intValue()); - assertEquals(2, trie.getBest(BufferUtil.toDirectBuffer("xHELxoxxxx"), 1, 8).intValue()); - assertEquals(3, trie.getBest(BufferUtil.toDirectBuffer("xHELLxxxxx"), 1, 8).intValue()); - assertEquals(6, trie.getBest(BufferUtil.toDirectBuffer("xfoo-BARxx"), 1, 8).intValue()); - assertEquals(8, trie.getBest(BufferUtil.toDirectBuffer("xHELL4xxxx"), 1, 8).intValue()); - assertEquals(9, trie.getBest(BufferUtil.toDirectBuffer("xZZZZZxxxx"), 1, 8).intValue()); + for (int i = 0; i < NOT_KEYS.length; i++) + { + String k = X_NOT_KEYS[i].toLowerCase(); + Integer actual = trie.getBest(BufferUtil.toDirectBuffer(k), 1, X_NOT_KEYS[i].length() - 1); + String[] expectations = trie.isCaseSensitive() ? BEST_NOT_KEYS_LOWER : BEST_NOT_KEYS; + Integer expected = expectations[i] == null ? null : trie.get(expectations[i]); + assertThat(k, actual, is(expected)); + } + } - ByteBuffer buffer = (ByteBuffer)BufferUtil.toDirectBuffer("xhelloxxxxxxx").position(2); - assertEquals(1, trie.getBest(buffer, -1, 10).intValue()); + @ParameterizedTest + @MethodSource("implementations") + public void testOtherChars(AbstractTrie trie) throws Exception + { + Assumptions.assumeTrue(trie instanceof ArrayTrie || trie instanceof TreeTrie); + assertTrue(trie.put("8859:ä", -1)); + assertTrue(trie.put("inv:\r\n", -2)); + assertTrue(trie.put("utf:\u20ac", -3)); + + assertThat(trie.getBest("8859:äxxxxx"), is(-1)); + assertThat(trie.getBest("inv:\r\n:xxxx"), is(-2)); + assertThat(trie.getBest("utf:\u20ac"), is(-3)); } @ParameterizedTest @@ -232,29 +300,98 @@ public class TrieTest @Test public void testRequiredCapacity() { - assertThat(requiredCapacity(Set.of("ABC", "abc"), true), is(6)); - assertThat(requiredCapacity(Set.of("ABC", "abc"), false), is(3)); - assertThat(requiredCapacity(Set.of(""), false), is(0)); - assertThat(requiredCapacity(Set.of("ABC", ""), false), is(3)); - assertThat(requiredCapacity(Set.of("ABC"), false), is(3)); - assertThat(requiredCapacity(Set.of("ABC", "XYZ"), false), is(6)); - assertThat(requiredCapacity(Set.of("A00", "A11"), false), is(5)); - assertThat(requiredCapacity(Set.of("A00", "A01", "A10", "A11"), false), is(7)); - assertThat(requiredCapacity(Set.of("A", "AB"), false), is(2)); - assertThat(requiredCapacity(Set.of("A", "ABC"), false), is(3)); - assertThat(requiredCapacity(Set.of("A", "ABCD"), false), is(4)); - assertThat(requiredCapacity(Set.of("AB", "ABC"), false), is(3)); - assertThat(requiredCapacity(Set.of("ABC", "ABCD"), false), is(4)); - assertThat(requiredCapacity(Set.of("ABC", "ABCDEF"), false), is(6)); - assertThat(requiredCapacity(Set.of("AB", "A"), false), is(2)); - assertThat(requiredCapacity(Set.of("ABC", "ABCDEF"), false), is(6)); - assertThat(requiredCapacity(Set.of("ABCDEF", "ABC"), false), is(6)); - assertThat(requiredCapacity(Set.of("ABC", "ABCDEF", "ABX"), false), is(7)); - assertThat(requiredCapacity(Set.of("ABCDEF", "ABC", "ABX"), false), is(7)); - assertThat(requiredCapacity(Set.of("ADEF", "AQPR4", "AQZ"), false), is(9)); - assertThat(requiredCapacity(Set.of("111", "ADEF", "AQPR4", "AQZ", "999"), false), is(15)); - assertThat(requiredCapacity(Set.of("utf-16", "utf-8"), false), is(7)); - assertThat(requiredCapacity(Set.of("utf-16", "utf-8", "utf16", "utf8"), false), is(10)); - assertThat(requiredCapacity(Set.of("utf-8", "utf8", "utf-16", "utf16", "iso-8859-1", "iso_8859_1"), false), is(27)); + assertThat(requiredCapacity(Set.of("ABC", "abc"), true), is(1 + 6)); + assertThat(requiredCapacity(Set.of("ABC", "abc"), false), is(1 + 3)); + assertThat(requiredCapacity(Set.of(""), false), is(1 + 0)); + assertThat(requiredCapacity(Set.of("ABC", ""), false), is(1 + 3)); + assertThat(requiredCapacity(Set.of("ABC"), false), is(1 + 3)); + assertThat(requiredCapacity(Set.of("ABC", "XYZ"), false), is(1 + 6)); + assertThat(requiredCapacity(Set.of("A00", "A11"), false), is(1 + 5)); + assertThat(requiredCapacity(Set.of("A00", "A01", "A10", "A11"), false), is(1 + 7)); + assertThat(requiredCapacity(Set.of("A", "AB"), false), is(1 + 2)); + assertThat(requiredCapacity(Set.of("A", "ABC"), false), is(1 + 3)); + assertThat(requiredCapacity(Set.of("A", "ABCD"), false), is(1 + 4)); + assertThat(requiredCapacity(Set.of("AB", "ABC"), false), is(1 + 3)); + assertThat(requiredCapacity(Set.of("ABC", "ABCD"), false), is(1 + 4)); + assertThat(requiredCapacity(Set.of("ABC", "ABCDEF"), false), is(1 + 6)); + assertThat(requiredCapacity(Set.of("AB", "A"), false), is(1 + 2)); + assertThat(requiredCapacity(Set.of("ABC", "ABCDEF"), false), is(1 + 6)); + assertThat(requiredCapacity(Set.of("ABCDEF", "ABC"), false), is(1 + 6)); + assertThat(requiredCapacity(Set.of("ABC", "ABCDEF", "ABX"), false), is(1 + 7)); + assertThat(requiredCapacity(Set.of("ABCDEF", "ABC", "ABX"), false), is(1 + 7)); + assertThat(requiredCapacity(Set.of("ADEF", "AQPR4", "AQZ"), false), is(1 + 9)); + assertThat(requiredCapacity(Set.of("111", "ADEF", "AQPR4", "AQZ", "999"), false), is(1 + 15)); + assertThat(requiredCapacity(Set.of("utf-16", "utf-8"), false), is(1 + 7)); + assertThat(requiredCapacity(Set.of("utf-16", "utf-8", "utf16", "utf8"), false), is(1 + 10)); + assertThat(requiredCapacity(Set.of("utf-8", "utf8", "utf-16", "utf16", "iso-8859-1", "iso_8859_1"), false), is(1 + 27)); + } + + @Test + public void testLargeRequiredCapacity() + { + String x = "x".repeat(Character.MAX_VALUE / 2); + String y = "y".repeat(Character.MAX_VALUE / 2); + String z = "z".repeat(Character.MAX_VALUE / 2); + assertThat(requiredCapacity(Set.of(x, y, z), true), is(1 + 3 * (Character.MAX_VALUE / 2))); + } + + @ParameterizedTest + @MethodSource("implementations") + public void testEmptyKey(AbstractTrie trie) throws Exception + { + assertTrue(trie.put("", -1)); + assertThat(trie.get(""), is(-1)); + assertThat(trie.getBest(""), is(-1)); + assertThat(trie.getBest("anything"), is(-1)); + assertThat(trie.getBest(BufferUtil.toBuffer("")), is(-1)); + assertThat(trie.getBest(BufferUtil.toBuffer("anything")), is(-1)); + assertThat(trie.getBest(BufferUtil.toBuffer("").array()), is(-1)); + assertThat(trie.getBest(BufferUtil.toBuffer("anything").array()), is(-1)); + + for (int i = 0; i < KEYS.length; i++) + { + assertThat(trie.get(KEYS[i]), is(i)); + assertThat(trie.getBest(KEYS[i] + "XYZ"), is(i)); + assertThat(trie.getBest(BufferUtil.toBuffer(KEYS[i] + "XYZ")), is(i)); + assertThat(trie.getBest(BufferUtil.toBuffer(KEYS[i] + "XYZ").array()), is(i)); + } + } + + @ParameterizedTest + @MethodSource("implementations") + public void testNullKey(AbstractTrie trie) throws Exception + { + assertThrows(NullPointerException.class, () -> trie.put(null, -1)); + } + + @ParameterizedTest + @MethodSource("implementations") + public void testNullValue(AbstractTrie trie) throws Exception + { + trie.put("null", 0); + assertTrue(trie.put("null", null)); + assertThat(trie.get("null"), nullValue()); + assertThat(trie.getBest("null;xxxx"), nullValue()); + } + + @ParameterizedTest + @MethodSource("implementations") + public void testNullChar(AbstractTrie trie) throws Exception + { + String key = "A" + ((char)0) + "c"; + trie.put(key, 103); + assertThat(trie.get(key), is(103)); + assertThat(trie.getBest(key + ";xxxx"), is(103)); + } + + @Test + public void testArrayTrieCapacity() + { + ArrayTrie trie = new ArrayTrie<>(Character.MAX_VALUE); + String huge = "x".repeat(Character.MAX_VALUE - 1); + assertTrue(trie.put(huge, "wow")); + assertThat(trie.get(huge), is("wow")); + + assertThrows(IllegalArgumentException.class, () -> new ArrayTrie(Character.MAX_VALUE + 1)); } } diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/TrieBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/TrieBenchmark.java new file mode 100644 index 00000000000..a56859c9e0f --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/TrieBenchmark.java @@ -0,0 +1,285 @@ +// +// ======================================================================== +// 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 v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jetty.http.HttpParser; +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.profile.GCProfiler; +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; +import org.openjdk.jmh.runner.options.TimeValue; + +@State(Scope.Benchmark) +public class TrieBenchmark +{ + @Param({ + "ArrayTrie", + "TernaryTrie", + "ArrayTernaryTrie", + "TreeTrie", + "HashTrie", + }) + public static String TRIE_TYPE; + + private AbstractTrie trie; + + private static final String LONG_HIT = "This-is-a-Moderately-Long-Key-that-will-hit"; + private static final String LONG_MISS = "This-is-a-Moderately-Long-Key-that-will-miss"; + + @Setup + public void setUp() throws Exception + { + boolean caseSensitive = false; + Set alphabet = new HashSet<>(); + int capacity = 4096; + + switch (TRIE_TYPE) + { + case "ArrayTrie": + trie = new ArrayTrie<>(caseSensitive, capacity); + break; + case "ArrayTernaryTrie": + trie = new ArrayTernaryTrie<>(caseSensitive, capacity); + break; + case "TreeTrie": + trie = new TreeTrie(); + break; + case "HashTrie": + trie = new HashTrie(caseSensitive); + break; + default: + throw new AssertionError("No trie for " + TRIE_TYPE); + } + + for (String k : HttpParser.CACHE.keySet()) + if (!trie.put(k, HttpParser.CACHE.get(k).toString())) + throw new IllegalStateException("Could not add " + k); + + trie.put(LONG_HIT, LONG_HIT); + +// System.err.println("===="); +// for (String k : trie.keySet()) +// System.err.printf("%s: %s%n", k, trie.get(k)); +// System.err.println("----"); + } + + @Benchmark + public boolean testPut() + { + trie.clear(); + for (String k : HttpParser.CACHE.keySet()) + if (!trie.put(k, HttpParser.CACHE.get(k).toString())) + return false; + return true; + } + + @Benchmark + public boolean testGet() + { + if ( + // short miss + trie.get("Xx") == null && + // long miss + trie.get("Zasdfadsfasfasfbae9mn3m0mdmmfkk092nvfs0smnsmm3k23m3m23m") == null && + + // short near miss + trie.get("Pragma: no-cache0") == null && + + // long near miss + trie.get(LONG_MISS) == null && + + // short hit + trie.get("Pragma: no-cache") != null && + + // medium hit + trie.get("Accept-Language: en-US,enq=0.5") != null && + + // long hit + trie.get(LONG_HIT) != null + ) + return true; + + throw new IllegalStateException(); + } + + private static final ByteBuffer X = BufferUtil.toBuffer("Xx\r\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + private static final ByteBuffer Z = BufferUtil.toBuffer("Zasdfadsfasfasfbae9mn3m0mdmmfkk092nvfs0smnsmm3k23m3m23m\r\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + private static final ByteBuffer M = BufferUtil.toBuffer(LONG_MISS + ";xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + private static final ByteBuffer P = BufferUtil.toBuffer("Pragma: no-cache;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + private static final ByteBuffer A = BufferUtil.toBuffer("Accept-Language: en-US,enq=0.5;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + private static final ByteBuffer H = BufferUtil.toBuffer(LONG_HIT + ";xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + + @Benchmark + public boolean testGetBest() + { + if ( + // short miss + trie.getBest(X) == null && + + // long miss + trie.getBest(Z) == null && + + // long near miss + trie.getBest(M) == null && + + // short hit + trie.getBest(P) != null && + + // medium hit + trie.getBest(A) != null && + + // long hit + trie.getBest(H) != null) + return true; + + throw new IllegalStateException(); + } + + private class HashTrie extends AbstractTrie + { + Map _contents; + + public HashTrie(boolean caseSensitive) + { + super(caseSensitive); + _contents = new HashMap<>(); + } + + @Override + public String get(String s) + { + if (isCaseInsensitive()) + s = StringUtil.asciiToLowerCase(s); + return _contents.get(s); + } + + @Override + public String get(String s, int offset, int len) + { + throw new UnsupportedOperationException(); + } + + @Override + public String get(ByteBuffer b, int offset, int len) + { + throw new UnsupportedOperationException(); + } + + @Override + public String getBest(String s, int offset, int len) + { + throw new UnsupportedOperationException(); + } + + @Override + public String getBest(ByteBuffer b, int offset, int len) + { + throw new UnsupportedOperationException(); + } + + @Override + public String getBest(ByteBuffer buf) + { + int len = buf.remaining(); + for (int i = 0; i < len; i++) + { + byte b = buf.get(buf.position() + i); + switch (b) + { + case '\r': + case '\n': + case ';': + String s = BufferUtil.toString(buf, buf.position(), i, StandardCharsets.ISO_8859_1); + if (isCaseInsensitive()) + s = StringUtil.asciiToLowerCase(s); + return trie.get(s); + default: + } + } + throw new IllegalStateException(); + } + + @Override + public boolean isEmpty() + { + throw new UnsupportedOperationException(); + } + + @Override + public int size() + { + throw new UnsupportedOperationException(); + } + + @Override + public Set keySet() + { + return _contents.keySet(); + } + + @Override + public boolean put(String s, String v) + { + if (isCaseInsensitive()) + s = StringUtil.asciiToLowerCase(s); + return _contents.put(s, v) == null; + } + + @Override + public void clear() + { + _contents.clear(); + } + } + +// public static void main(String... args) throws Exception +// { +// TrieBenchmark.TRIE_TYPE = "HashTrie"; +// TrieBenchmark b = new TrieBenchmark(); +// b.setUp(); +// b.testGet(); +// b.testGetBest(); +// b.testPut(); +// } + + public static void main(String[] args) throws RunnerException + { + Options opt = new OptionsBuilder() + .include(TrieBenchmark.class.getSimpleName()) + .warmupIterations(3) + .warmupTime(TimeValue.seconds(5)) + .measurementIterations(3) + .measurementTime(TimeValue.seconds(5)) + .forks(1) + .threads(1) + .addProfiler(GCProfiler.class) + .build(); + + new Runner(opt).run(); + } + +}