MutableHttpFields.asImmutable avoids copy (#10651)

* Avoid a copy in MutableHttpFields.asImmutable if the mutable is never mutated again.
* reduce instance creations needed for iterations
---------
Co-authored-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Greg Wilkins 2023-10-16 23:58:19 +02:00 committed by GitHub
parent df6995e125
commit ffe80cd1f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 412 additions and 127 deletions

View File

@ -470,7 +470,7 @@ public class ContextProvider extends ScanningAppProvider
contextPath = "/";
}
// handle root with virtual host form
else if (StringUtil.startsWithIgnoreCase(contextPath, "root-"))
else if (StringUtil.asciiStartsWithIgnoreCase(contextPath, "root-"))
{
int dash = contextPath.indexOf('-');
String virtual = contextPath.substring(dash + 1);

View File

@ -46,7 +46,7 @@ public final class HttpCompliance implements ComplianceViolation.Mode
/**
* The HTTP RFC(s) require that field names are case-insensitive, so for example the fields "{@code Content-Type: text/xml}"
* and "{@code content-type: text/xml}" are considered equivalent. Jetty has been optimized to take advantage of this by
* looking up field names in a case insensitive cache and will by default provide the standard capitalisation of a field
* looking up field names in a case-insensitive cache and will by default provide the standard capitalisation of a field
* name rather than create a new string with the actual capitalisation received. However, some applications have been
* written to expect a specific capitalisation of field, so deployments of such applications must include this violation
* in their {@link HttpCompliance} mode to prevent Jetty altering the case of the fields received. Jetty itself will still
@ -56,8 +56,8 @@ public final class HttpCompliance implements ComplianceViolation.Mode
CASE_SENSITIVE_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Field name is case-insensitive"),
/**
* The HTTP RFC(s) require that method names are case sensitive, so that "{@code Get}" and "{@code GET}" are considered
* different methods. Jetty releases prior to 9.4 used a case insensitive cache to match method names, thus this requirement
* The HTTP RFC(s) require that method names are case-sensitive, so that "{@code Get}" and "{@code GET}" are considered
* different methods. Jetty releases prior to 9.4 used a case-insensitive cache to match method names, thus this requirement
* was violated. Deployments which wish to retain this legacy violation can include this violation in the
* {@link HttpCompliance} mode.
*/

View File

@ -194,7 +194,7 @@ public enum HttpHeader
public boolean is(String s)
{
return _string.equalsIgnoreCase(s);
return StringUtil.asciiEqualsIgnoreCase(_string, s);
}
/**

View File

@ -95,7 +95,7 @@ public enum HttpMethod
public boolean is(String s)
{
return toString().equalsIgnoreCase(s);
return name().equals(s);
}
/**

View File

@ -653,22 +653,20 @@ public class HttpParser
_length = _string.length();
_methodString = takeString();
if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode))
HttpMethod method = HttpMethod.CACHE.get(_methodString);
if (method != null)
{
HttpMethod method = HttpMethod.INSENSITIVE_CACHE.get(_methodString);
_methodString = method.asString();
}
else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode))
{
method = HttpMethod.INSENSITIVE_CACHE.get(_methodString);
if (method != null)
{
if (!method.asString().equals(_methodString))
reportComplianceViolation(Violation.CASE_INSENSITIVE_METHOD, _methodString);
_methodString = method.asString();
reportComplianceViolation(Violation.CASE_INSENSITIVE_METHOD, _methodString);
}
}
else
{
HttpMethod method = HttpMethod.CACHE.get(_methodString);
if (method != null)
_methodString = method.asString();
}
setState(State.SPACE1);
break;

View File

@ -20,6 +20,7 @@ import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import org.eclipse.jetty.http.UriCompliance.Violation;
import org.eclipse.jetty.util.HostPort;
@ -283,7 +284,7 @@ public interface HttpURI
private final String _fragment;
private String _uri;
private String _canonicalPath;
private final EnumSet<Violation> _violations = EnumSet.noneOf(Violation.class);
private Set<Violation> _violations;
private Immutable(Mutable builder)
{
@ -297,7 +298,8 @@ public interface HttpURI
_fragment = builder._fragment;
_uri = builder._uri;
_canonicalPath = builder._canonicalPath;
_violations.addAll(builder._violations);
if (builder._violations != null)
_violations = Collections.unmodifiableSet(EnumSet.copyOf(builder._violations));
}
private Immutable(String uri)
@ -470,25 +472,25 @@ public interface HttpURI
@Override
public boolean isAmbiguous()
{
return UriCompliance.isAmbiguous(_violations);
return _violations != null && UriCompliance.isAmbiguous(_violations);
}
@Override
public boolean hasViolations()
{
return !_violations.isEmpty();
return _violations != null && !_violations.isEmpty();
}
@Override
public boolean hasViolation(Violation violation)
{
return _violations.contains(violation);
return _violations != null && _violations.contains(violation);
}
@Override
public Collection<Violation> getViolations()
{
return Collections.unmodifiableCollection(_violations);
return _violations == null ? Collections.emptySet() : _violations;
}
@Override
@ -566,7 +568,7 @@ public interface HttpURI
private String _fragment;
private String _uri;
private String _canonicalPath;
private final EnumSet<Violation> _violations = EnumSet.noneOf(Violation.class);
private Set<Violation> _violations;
private boolean _emptySegment;
private Mutable()
@ -709,7 +711,8 @@ public interface HttpURI
_uri = null;
_canonicalPath = null;
_emptySegment = false;
_violations.clear();
if (_violations != null)
_violations.clear();
return this;
}
@ -844,25 +847,25 @@ public interface HttpURI
@Override
public boolean isAmbiguous()
{
return UriCompliance.isAmbiguous(_violations);
return _violations != null && UriCompliance.isAmbiguous(_violations);
}
@Override
public boolean hasViolations()
{
return !_violations.isEmpty();
return _violations != null && !_violations.isEmpty();
}
@Override
public boolean hasViolation(Violation violation)
{
return _violations.contains(violation);
return _violations != null && _violations.contains(violation);
}
@Override
public Collection<Violation> getViolations()
{
return Collections.unmodifiableCollection(_violations);
return _violations == null ? Collections.emptySet() : Collections.unmodifiableCollection(_violations);
}
public Mutable normalize()
@ -991,7 +994,9 @@ public interface HttpURI
_query = uri.getQuery();
_uri = null;
_canonicalPath = uri.getCanonicalPath();
_violations.addAll(uri.getViolations());
Collection<Violation> violations = uri.getViolations();
if (!violations.isEmpty())
_violations = EnumSet.copyOf(violations);
return this;
}
@ -1265,7 +1270,7 @@ public interface HttpURI
{
if (encodedCharacters == 2 && c == 'u' && !encodedUtf16)
{
_violations.add(Violation.UTF16_ENCODINGS);
addViolation(Violation.UTF16_ENCODINGS);
encodedUtf16 = true;
encodedCharacters = 4;
continue;
@ -1281,10 +1286,10 @@ public interface HttpURI
// other than as the NUL ASCII byte which we do not wish to allow.
throw new IllegalArgumentException("Illegal character in path");
case '/':
_violations.add(Violation.AMBIGUOUS_PATH_SEPARATOR);
addViolation(Violation.AMBIGUOUS_PATH_SEPARATOR);
break;
case '%':
_violations.add(Violation.AMBIGUOUS_PATH_ENCODING);
addViolation(Violation.AMBIGUOUS_PATH_ENCODING);
break;
default:
break;
@ -1448,7 +1453,7 @@ public interface HttpURI
private RuntimeException onBadUtf8()
{
// We just remember the violation and return null so nothing is thrown
_violations.add(Violation.BAD_UTF8_ENCODING);
addViolation(Violation.BAD_UTF8_ENCODING);
return null;
}
@ -1470,7 +1475,7 @@ public interface HttpURI
// So if this method is called for any segment and we have previously
// seen an empty segment, then it was ambiguous.
if (_emptySegment)
_violations.add(Violation.AMBIGUOUS_EMPTY_SEGMENT);
addViolation(Violation.AMBIGUOUS_EMPTY_SEGMENT);
if (end == segment)
{
@ -1481,7 +1486,7 @@ public interface HttpURI
// If this empty segment is the first segment then it is ambiguous.
if (segment == 0)
{
_violations.add(Violation.AMBIGUOUS_EMPTY_SEGMENT);
addViolation(Violation.AMBIGUOUS_EMPTY_SEGMENT);
return;
}
@ -1499,11 +1504,19 @@ public interface HttpURI
{
// The segment is always ambiguous.
if (Boolean.TRUE.equals(ambiguous))
_violations.add(Violation.AMBIGUOUS_PATH_SEGMENT);
addViolation(Violation.AMBIGUOUS_PATH_SEGMENT);
// The segment is ambiguous only when followed by a parameter.
if (param)
_violations.add(Violation.AMBIGUOUS_PATH_PARAMETER);
addViolation(Violation.AMBIGUOUS_PATH_PARAMETER);
}
}
private void addViolation(Violation violation)
{
if (_violations == null)
_violations = EnumSet.of(violation);
else
_violations.add(violation);
}
}
}

View File

@ -44,7 +44,6 @@ class ImmutableHttpFields implements HttpFields
protected ImmutableHttpFields(HttpField[] fields, int size)
{
Objects.requireNonNull(fields);
_fields = fields;
_size = size;
}
@ -58,8 +57,8 @@ class ImmutableHttpFields implements HttpFields
@Override
public int hashCode()
{
int hash = 0;
for (int i = _fields.length; i-- > 0; )
int hash = 1993; // prime
for (int i = _size; i-- > 0; )
{
hash ^= _fields[i].hashCode();
}
@ -128,7 +127,7 @@ class ImmutableHttpFields implements HttpFields
@Override
public HttpField getField(int index)
{
if (index >= _fields.length)
if (index >= _size)
throw new NoSuchElementException();
return _fields[index];
}

View File

@ -40,6 +40,7 @@ class MutableHttpFields implements HttpFields.Mutable
private static final int SIZE_INCREMENT = 4;
private HttpField[] _fields;
private boolean _immutable;
private int _size;
/**
@ -67,7 +68,21 @@ class MutableHttpFields implements HttpFields.Mutable
*/
protected MutableHttpFields(HttpFields fields)
{
add(fields);
if (fields instanceof ImmutableHttpFields immutable)
{
_immutable = true;
_fields = immutable._fields;
_size = immutable._size;
}
else if (fields != null)
{
_fields = new HttpField[fields.size() + SIZE_INCREMENT];
add(fields);
}
else
{
_fields = new HttpField[INITIAL_SIZE];
}
}
/**
@ -120,10 +135,11 @@ class MutableHttpFields implements HttpFields.Mutable
{
if (field != null)
{
if (_fields == null)
_fields = new HttpField[INITIAL_SIZE];
if (_size == _fields.length)
if (_immutable || _size == _fields.length)
{
_immutable = false;
_fields = Arrays.copyOf(_fields, _size + SIZE_INCREMENT);
}
_fields[_size++] = field;
}
return this;
@ -132,14 +148,15 @@ class MutableHttpFields implements HttpFields.Mutable
@Override
public Mutable add(HttpFields fields)
{
if (_fields == null)
_fields = new HttpField[fields.size() + SIZE_INCREMENT];
else if (_size + fields.size() >= _fields.length)
_fields = Arrays.copyOf(_fields, _size + fields.size() + SIZE_INCREMENT);
if (fields.size() == 0)
return this;
if (_immutable || _size + fields.size() >= _fields.length)
{
_immutable = false;
_fields = Arrays.copyOf(_fields, _size + fields.size() + SIZE_INCREMENT);
}
if (fields instanceof org.eclipse.jetty.http.ImmutableHttpFields immutable)
{
System.arraycopy(immutable._fields, 0, _fields, _size, immutable._size);
@ -163,12 +180,27 @@ class MutableHttpFields implements HttpFields.Mutable
@Override
public HttpFields asImmutable()
{
return new org.eclipse.jetty.http.ImmutableHttpFields(Arrays.copyOf(_fields, _size));
_immutable = true;
return new ImmutableHttpFields(_fields, _size);
}
private void copyImmutable()
{
if (_immutable)
{
_immutable = false;
_fields = Arrays.copyOf(_fields, _fields.length);
}
}
@Override
public Mutable clear()
{
if (_immutable)
{
_fields = new HttpField[_fields.length];
_immutable = false;
}
_size = 0;
return this;
}
@ -176,7 +208,7 @@ class MutableHttpFields implements HttpFields.Mutable
@Override
public int hashCode()
{
int hash = 0;
int hash = 2099; // prime
for (int i = _size; i-- > 0; )
{
HttpField field = _fields[i];
@ -211,6 +243,32 @@ class MutableHttpFields implements HttpFields.Mutable
return _fields[index];
}
@Override
public HttpField getField(HttpHeader header)
{
// default impl overridden for efficiency
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f != null && f.getHeader() == header)
return f;
}
return null;
}
@Override
public HttpField getField(String name)
{
// default impl overridden for efficiency
for (int i = 0; i < _size; i++)
{
HttpField f = _fields[i];
if (f != null && f.is(name))
return f;
}
return null;
}
@Override
public Iterator<HttpField> iterator()
{
@ -243,18 +301,20 @@ class MutableHttpFields implements HttpFields.Mutable
@Override
public ListIterator<HttpField> listIterator()
{
return new Listerator(0);
return listIterator(0);
}
@Override
public ListIterator<HttpField> listIterator(int index)
{
copyImmutable();
return new Listerator(index);
}
@Override
public Mutable put(HttpField field)
{
copyImmutable();
boolean put = false;
for (int i = 0; i < _size; i++)
@ -326,6 +386,7 @@ class MutableHttpFields implements HttpFields.Mutable
public <T> Mutable computeField(T header, BiFunction<T, List<HttpField>, HttpField> computeFn, BiPredicate<HttpField, T> matcher)
{
copyImmutable();
// Look for first occurrence
int first = -1;
for (int i = 0; i < _size; i++)
@ -418,7 +479,18 @@ class MutableHttpFields implements HttpFields.Mutable
private void remove(int i)
{
_size--;
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
if (_immutable)
{
_immutable = false;
HttpField[] fields = _fields;
_fields = new HttpField[fields.length];
System.arraycopy(fields, 0, _fields, 0, i);
System.arraycopy(fields, i + 1, _fields, i, _size - i);
}
else
{
System.arraycopy(_fields, i + 1, _fields, i, _size - i);
}
_fields[_size] = null;
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.http;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
@ -127,12 +128,13 @@ public final class UriCompliance implements ComplianceViolation.Mode
}
}
public static final EnumSet<Violation> AMBIGUOUS_VIOLATIONS = EnumSet.of(
public static final Set<Violation> NO_VIOLATION = Collections.unmodifiableSet(EnumSet.noneOf(Violation.class));
public static final Set<Violation> AMBIGUOUS_VIOLATIONS = Collections.unmodifiableSet(EnumSet.of(
Violation.AMBIGUOUS_EMPTY_SEGMENT,
Violation.AMBIGUOUS_PATH_ENCODING,
Violation.AMBIGUOUS_PATH_PARAMETER,
Violation.AMBIGUOUS_PATH_SEGMENT,
Violation.AMBIGUOUS_PATH_SEPARATOR);
Violation.AMBIGUOUS_PATH_SEPARATOR));
/**
* Compliance mode that exactly follows <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>,
@ -143,7 +145,8 @@ public final class UriCompliance implements ComplianceViolation.Mode
/**
* Compliance mode that allows all unambiguous violations.
*/
public static final UriCompliance UNAMBIGUOUS = new UriCompliance("UNAMBIGUOUS", complementOf(AMBIGUOUS_VIOLATIONS));
public static final UriCompliance UNAMBIGUOUS = new UriCompliance("UNAMBIGUOUS",
complementOf(EnumSet.copyOf(AMBIGUOUS_VIOLATIONS)));
/**
* The default compliance mode allows no violations from <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>
@ -171,7 +174,7 @@ public final class UriCompliance implements ComplianceViolation.Mode
private static final AtomicInteger __custom = new AtomicInteger();
private static final List<UriCompliance> KNOWN_MODES = List.of(DEFAULT, LEGACY, RFC3986, UNAMBIGUOUS, UNSAFE);
public static boolean isAmbiguous(EnumSet<Violation> violations)
public static boolean isAmbiguous(Set<Violation> violations)
{
if (violations.isEmpty())
return false;
@ -277,7 +280,7 @@ public final class UriCompliance implements ComplianceViolation.Mode
{
Objects.requireNonNull(violations);
_name = name;
_allowed = unmodifiableSet(violations.isEmpty() ? noneOf(Violation.class) : copyOf(violations));
_allowed = violations.isEmpty() ? NO_VIOLATION : unmodifiableSet(copyOf(violations));
}
@Override

View File

@ -25,6 +25,7 @@ import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -32,6 +33,7 @@ import org.eclipse.jetty.util.BufferUtil;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
@ -179,6 +181,101 @@ public class HttpFieldsTest
assertThrows(NoSuchElementException.class, () -> header.getField(2));
}
public static Stream<Arguments> afterAsImmutable()
{
return Stream.of(
Arguments.of(
(Consumer<HttpFields.Mutable>)m -> m.remove("name0"),
(Consumer<HttpFields.Mutable>)m ->
{
assertThat(m.size(), is(1));
assertThat(m.get("name1"), is("value1"));
}
),
Arguments.of(
(Consumer<HttpFields.Mutable>)m -> m.remove("name1"),
(Consumer<HttpFields.Mutable>)m ->
{
assertThat(m.size(), is(1));
assertThat(m.get("name0"), is("value0"));
}
),
Arguments.of(
(Consumer<HttpFields.Mutable>)m ->
{
ListIterator<HttpField> i = m.listIterator();
i.next();
i.remove();
},
(Consumer<HttpFields.Mutable>)m ->
{
assertThat(m.size(), is(1));
assertThat(m.get("name1"), is("value1"));
}
),
Arguments.of(
(Consumer<HttpFields.Mutable>)m -> m.remove("name2"),
(Consumer<HttpFields.Mutable>)m ->
{
assertThat(m.size(), is(2));
assertThat(m.get("name0"), is("value0"));
assertThat(m.get("name1"), is("value1"));
}
),
Arguments.of(
(Consumer<HttpFields.Mutable>)m -> m.add("name2", "value2"),
(Consumer<HttpFields.Mutable>)m ->
{
assertThat(m.size(), is(3));
assertThat(m.get("name0"), is("value0"));
assertThat(m.get("name1"), is("value1"));
assertThat(m.get("name2"), is("value2"));
}
),
Arguments.of(
(Consumer<HttpFields.Mutable>)m -> m.put("name2", "value2"),
(Consumer<HttpFields.Mutable>)m ->
{
assertThat(m.size(), is(3));
assertThat(m.get("name0"), is("value0"));
assertThat(m.get("name1"), is("value1"));
assertThat(m.get("name2"), is("value2"));
}
),
Arguments.of(
(Consumer<HttpFields.Mutable>)m -> m.put("name1", "ONE"),
(Consumer<HttpFields.Mutable>)m ->
{
assertThat(m.size(), is(2));
assertThat(m.get("name0"), is("value0"));
assertThat(m.get("name1"), is("ONE"));
}
)
);
}
@ParameterizedTest
@MethodSource("afterAsImmutable")
public void testMutationAfterAsImmutable(Consumer<HttpFields.Mutable> mutation, Consumer<HttpFields.Mutable> check)
{
HttpFields.Mutable mutable = HttpFields.build();
HttpFields immutable = mutable
.put("name0", "value0")
.put("name1", "value1").asImmutable();
assertThat(immutable.size(), is(2));
assertThat(immutable.get("name0"), is("value0"));
assertThat(immutable.get("name1"), is("value1"));
mutation.accept(mutable);
assertThat(immutable.size(), is(2));
assertThat(immutable.get("name0"), is("value0"));
assertThat(immutable.get("name1"), is("value1"));
check.accept(mutable);
}
@ParameterizedTest
@MethodSource("mutables")
public void testMutable(HttpFields.Mutable mutable)

View File

@ -479,10 +479,10 @@ public class ArrayByteBufferPool implements ByteBufferPool, Dumpable
private RetainedBucket(int capacity, int poolSize)
{
if (poolSize <= ConcurrentPool.OPTIMAL_MAX_SIZE)
_pool = new ConcurrentPool<>(ConcurrentPool.StrategyType.THREAD_ID, poolSize, true);
_pool = new ConcurrentPool<>(ConcurrentPool.StrategyType.THREAD_ID, poolSize, false);
else
_pool = new CompoundPool<>(
new ConcurrentPool<>(ConcurrentPool.StrategyType.THREAD_ID, ConcurrentPool.OPTIMAL_MAX_SIZE, true),
new ConcurrentPool<>(ConcurrentPool.StrategyType.THREAD_ID, ConcurrentPool.OPTIMAL_MAX_SIZE, false),
new QueuedPool<>(poolSize - ConcurrentPool.OPTIMAL_MAX_SIZE)
);
_capacity = capacity;

View File

@ -556,8 +556,11 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable
if (LOG.isDebugEnabled())
LOG.debug("updateable {}", _updateable.size());
for (SelectorUpdate update : _updateable)
while (true)
{
SelectorUpdate update = _updateable.pollFirst();
if (update == null)
break;
if (_selector == null)
break;
try
@ -571,7 +574,6 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable
LOG.warn("Cannot update selector {}", ManagedSelector.this, x);
}
}
_updateable.clear();
Selector selector;
int updates;

View File

@ -326,7 +326,7 @@ public class ConnectorServer extends AbstractLifeCycle
@Override
public int hashCode()
{
return _host != null ? _host.hashCode() : 0;
return _host != null ? _host.hashCode() : 101;
}
@Override

View File

@ -1123,7 +1123,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
protected class HttpStreamOverHTTP1 implements HttpStream
{
private final String _id;
private final long _id;
private final String _method;
private final HttpURI.Mutable _uri;
private final HttpVersion _version;
@ -1141,10 +1141,10 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
protected HttpStreamOverHTTP1(String method, String uri, HttpVersion version)
{
_id = Objects.requireNonNull(version).toString() + '#' + _streamIdGenerator.getAndIncrement();
_id = _streamIdGenerator.getAndIncrement();
_method = method;
_uri = uri == null ? null : HttpURI.build(method, uri);
_version = version;
_version = Objects.requireNonNull(version);
if (_uri != null && _uri.getPath() == null && _uri.getScheme() != null && _uri.hasAuthority())
_uri.path("/");
@ -1364,7 +1364,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
@Override
public String getId()
{
return _id;
return Long.toString(_id);
}
@Override

View File

@ -57,6 +57,18 @@ public class ResponseHttpFields implements HttpFields.Mutable
_fields.clear();
}
@Override
public HttpField getField(String name)
{
return _fields.getField(name);
}
@Override
public HttpField getField(HttpHeader header)
{
return _fields.getField(header);
}
@Override
public HttpField getField(int index)
{
@ -116,9 +128,9 @@ public class ResponseHttpFields implements HttpFields.Mutable
@Override
public Iterator<HttpField> iterator()
{
Iterator<HttpField> i = _fields.iterator();
return new Iterator<>()
{
private final Iterator<HttpField> i = _fields.iterator();
private HttpField _current;
@Override

View File

@ -544,7 +544,7 @@ public interface Attributes
@Override
public int hashCode()
{
int hash = 0;
int hash = 113;
for (String name : getAttributeNameSet())
hash += name.hashCode() ^ getAttribute(name).hashCode();
return hash;

View File

@ -221,7 +221,7 @@ public class ConcurrentPool<P> implements Pool<P>, Dumpable
case FIRST -> 0;
case RANDOM -> ThreadLocalRandom.current().nextInt(size);
case ROUND_ROBIN -> nextIndex.getAndUpdate(c -> Math.max(0, c + 1)) % size;
case THREAD_ID -> (int)(Thread.currentThread().getId() % size);
case THREAD_ID -> (int)((Thread.currentThread().getId() * 31) % size);
};
}

View File

@ -225,18 +225,27 @@ public class StringUtil
return String.valueOf(chars);
}
public static boolean startsWithIgnoreCase(String s, String w)
/**
* Check for string equality, ignoring {@link StandardCharsets#US_ASCII} case differences.
* @param string The string to check
* @param other The other string to check
* @return true if the strings are equal, ignoring {@link StandardCharsets#US_ASCII} case differences.
*/
public static boolean asciiEqualsIgnoreCase(String string, String other)
{
if (w == null)
return true;
if (string == null)
return other == null;
if (s == null || s.length() < w.length())
if (other == null)
return false;
for (int i = 0; i < w.length(); i++)
if (string.length() != other.length())
return false;
for (int i = 0; i < string.length(); i++)
{
char c1 = s.charAt(i);
char c2 = w.charAt(i);
char c1 = string.charAt(i);
char c2 = other.charAt(i);
if (c1 != c2)
{
if (c1 <= 127)
@ -250,23 +259,86 @@ public class StringUtil
return true;
}
public static boolean endsWithIgnoreCase(String s, String w)
/**
* Check for a string prefix, ignoring {@link StandardCharsets#US_ASCII} case differences.
* @param string The string to check
* @param prefix The sub string to look for as a prefix
* @return true if the string ends with the substring, ignoring {@link StandardCharsets#US_ASCII} case differences.
* @deprecated Use {@link #asciiEndsWithIgnoreCase(String, String)}
*/
@Deprecated
public static boolean startsWithIgnoreCase(String string, String prefix)
{
if (w == null)
return asciiStartsWithIgnoreCase(string, prefix);
}
/**
* Check for a string prefix, ignoring {@link StandardCharsets#US_ASCII} case differences.
* @param string The string to check
* @param prefix The sub string to look for as a prefix
* @return true if the string ends with the substring, ignoring {@link StandardCharsets#US_ASCII} case differences.
*/
public static boolean asciiStartsWithIgnoreCase(String string, String prefix)
{
if (isEmpty(prefix))
return true;
if (s == null)
if (string == null || string.length() < prefix.length())
return false;
int sl = s.length();
int wl = w.length();
if (sl < wl)
return false;
for (int i = wl; i-- > 0; )
for (int i = 0; i < prefix.length(); i++)
{
char c1 = s.charAt(--sl);
char c2 = w.charAt(i);
char c1 = string.charAt(i);
char c2 = prefix.charAt(i);
if (c1 != c2)
{
if (c1 <= 127)
c1 = LOWERCASES[c1];
if (c2 <= 127)
c2 = LOWERCASES[c2];
if (c1 != c2)
return false;
}
}
return true;
}
/**
* Check for a string suffix, ignoring {@link StandardCharsets#US_ASCII} case differences.
* @param string The string to check
* @param suffix The sub string to look for as a suffix
* @return true if the string ends with the substring, ignoring {@link StandardCharsets#US_ASCII} case differences.
* @deprecated Use {@link #asciiEndsWithIgnoreCase(String, String)}
*/
@Deprecated
public static boolean endsWithIgnoreCase(String string, String suffix)
{
return asciiEndsWithIgnoreCase(string, suffix);
}
/**
* Check for a string suffix, ignoring {@link StandardCharsets#US_ASCII} case differences.
* @param string The string to check
* @param suffix The sub string to look for as a suffix
* @return true if the string ends with the substring, ignoring {@link StandardCharsets#US_ASCII} case differences.
*/
public static boolean asciiEndsWithIgnoreCase(String string, String suffix)
{
if (isEmpty(suffix))
return true;
if (string == null)
return false;
int stringLength = string.length();
int suffixLength = suffix.length();
if (stringLength < suffixLength)
return false;
for (int i = suffixLength; i-- > 0; )
{
char c1 = string.charAt(--stringLength);
char c2 = suffix.charAt(i);
if (c1 != c2)
{
if (c1 <= 127)

View File

@ -29,55 +29,65 @@ import static org.hamcrest.Matchers.sameInstance;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
public class StringUtilTest
{
@Test
@SuppressWarnings("ReferenceEquality")
public void testAsciiToLowerCase()
{
String lc = "\u0690bc def 1\u06903";
assertEquals(StringUtil.asciiToLowerCase("\u0690Bc DeF 1\u06903"), lc);
assertTrue(StringUtil.asciiToLowerCase(lc) == lc);
assertSame(StringUtil.asciiToLowerCase(lc), lc);
}
@Test
public void testStartsWithIgnoreCase()
public void testAsciiEqualsIgnoreCase()
{
assertTrue(StringUtil.asciiEqualsIgnoreCase(null, null));
assertTrue(StringUtil.asciiEqualsIgnoreCase("", ""));
assertTrue(StringUtil.asciiEqualsIgnoreCase("AbC", "aBc"));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690b\u0690defg", "\u0690b\u0690"));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "\u0690bc"));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "\u0690Bc"));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690Bcdefg", "\u0690bc"));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690Bcdefg", "\u0690Bc"));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", ""));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", null));
assertTrue(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "\u0690bcdefg"));
assertFalse(StringUtil.startsWithIgnoreCase(null, "xyz"));
assertFalse(StringUtil.startsWithIgnoreCase("\u0690bcdefg", "xyz"));
assertFalse(StringUtil.startsWithIgnoreCase("\u0690", "xyz"));
assertFalse(StringUtil.asciiEqualsIgnoreCase("AbC", "aBcd"));
assertFalse(StringUtil.asciiEqualsIgnoreCase("AbCd", "aBc"));
}
@Test
public void testEndsWithIgnoreCase()
public void testAsciiStartsWithIgnoreCase()
{
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcd\u0690f\u0690", "\u0690f\u0690"));
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "efg"));
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "eFg"));
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdeFg", "efg"));
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdeFg", "eFg"));
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", ""));
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", null));
assertTrue(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "\u0690bcdefg"));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690b\u0690defg", "\u0690b\u0690"));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690bcdefg", "\u0690bc"));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690bcdefg", "\u0690Bc"));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690Bcdefg", "\u0690bc"));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690Bcdefg", "\u0690Bc"));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690bcdefg", ""));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690bcdefg", null));
assertTrue(StringUtil.asciiStartsWithIgnoreCase("\u0690bcdefg", "\u0690bcdefg"));
assertFalse(StringUtil.endsWithIgnoreCase(null, "xyz"));
assertFalse(StringUtil.endsWithIgnoreCase("\u0690bcdefg", "xyz"));
assertFalse(StringUtil.endsWithIgnoreCase("\u0690", "xyz"));
assertFalse(StringUtil.asciiStartsWithIgnoreCase(null, "xyz"));
assertFalse(StringUtil.asciiStartsWithIgnoreCase("\u0690bcdefg", "xyz"));
assertFalse(StringUtil.asciiStartsWithIgnoreCase("\u0690", "xyz"));
}
@Test
public void testAsciiEndsWithIgnoreCase()
{
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcd\u0690f\u0690", "\u0690f\u0690"));
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdefg", "efg"));
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdefg", "eFg"));
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdeFg", "efg"));
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdeFg", "eFg"));
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdefg", ""));
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdefg", null));
assertTrue(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdefg", "\u0690bcdefg"));
assertFalse(StringUtil.asciiEndsWithIgnoreCase(null, "xyz"));
assertFalse(StringUtil.asciiEndsWithIgnoreCase("\u0690bcdefg", "xyz"));
assertFalse(StringUtil.asciiEndsWithIgnoreCase("\u0690", "xyz"));
}
@Test

View File

@ -88,7 +88,6 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
throw new IllegalStateException("could not find %s for %s".formatted(ServletContextRequest.class.getSimpleName(), request));
}
private final List<ServletRequestAttributeListener> _requestAttributeListeners = new ArrayList<>();
private final ServletApiRequest _servletApiRequest;
private final ServletContextResponse _response;
private final MatchedResource<ServletHandler.MappedServlet> _matchedResource;
@ -96,6 +95,7 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
private final String _decodedPathInContext;
private final ServletChannel _servletChannel;
private final SessionManager _sessionManager;
private List<ServletRequestAttributeListener> _requestAttributeListeners;
private Charset _queryEncoding;
private HttpFields _trailers;
private ManagedSession _managedSession;
@ -389,20 +389,27 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
@Override
public List<ServletRequestAttributeListener> getRequestAttributeListeners()
{
if (_requestAttributeListeners == null)
_requestAttributeListeners = new ArrayList<>();
return _requestAttributeListeners;
}
public void addEventListener(final EventListener listener)
public void addEventListener(EventListener listener)
{
if (listener instanceof ServletRequestAttributeListener)
_requestAttributeListeners.add((ServletRequestAttributeListener)listener);
if (listener instanceof ServletRequestAttributeListener attributeListener)
{
if (_requestAttributeListeners == null)
_requestAttributeListeners = new ArrayList<>();
_requestAttributeListeners.add(attributeListener);
}
if (listener instanceof AsyncListener)
throw new IllegalArgumentException(listener.getClass().toString());
}
public void removeEventListener(final EventListener listener)
public void removeEventListener(EventListener listener)
{
_requestAttributeListeners.remove(listener);
if (_requestAttributeListeners != null)
_requestAttributeListeners.remove(listener);
}
/**

View File

@ -104,7 +104,7 @@ public class JakartaWebSocketExtension implements Extension
@Override
public int hashCode()
{
return name != null ? name.hashCode() : 0;
return name != null ? name.hashCode() : 41;
}
@Override

View File

@ -189,7 +189,7 @@ public class JakartaWebSocketCreator implements WebSocketCreator
@Override
public int hashCode()
{
return (baseConfig != null ? baseConfig.hashCode() : 0);
return (baseConfig != null ? baseConfig.hashCode() : 47);
}
@Override

View File

@ -104,7 +104,7 @@ public class JakartaWebSocketExtension implements Extension
@Override
public int hashCode()
{
return name != null ? name.hashCode() : 0;
return name != null ? name.hashCode() : 79;
}
@Override

View File

@ -189,7 +189,7 @@ public class JakartaWebSocketCreator implements WebSocketCreator
@Override
public int hashCode()
{
return (baseConfig != null ? baseConfig.hashCode() : 0);
return (baseConfig != null ? baseConfig.hashCode() : 83);
}
@Override