Merge branch '7.0.x'

This commit is contained in:
rstoyanchev
2026-05-01 21:48:29 +01:00
12 changed files with 120 additions and 61 deletions
@@ -138,7 +138,6 @@ dependencies {
api("org.seleniumhq.selenium:selenium-java:4.41.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.testng:testng:7.12.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-lite:1.1.0")
api("org.xmlunit:xmlunit-assertj:2.10.4")
api("org.xmlunit:xmlunit-matchers:2.10.4")
@@ -238,7 +238,7 @@ public abstract class MimeTypeUtils {
break;
}
}
else if (ch == '"') {
else if (ch == '"' && mimeType.charAt(nextIndex - 1) != '\\') {
quoted = !quoted;
}
nextIndex++;
@@ -98,7 +98,7 @@ class MimeTypeTests {
}
@Test
void parseQuotedSeparator() {
void parseQuotedParameterValue() {
String s = "application/xop+xml;charset=utf-8;type=\"application/soap+xml;action=\\\"https://x.y.z\\\"\"";
MimeType mimeType = MimeType.valueOf(s);
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application");
@@ -107,6 +107,15 @@ class MimeTypeTests {
assertThat(mimeType.getParameter("type")).isEqualTo("\"application/soap+xml;action=\\\"https://x.y.z\\\"\"");
}
@Test
void parseParameterWithQuotedPair() {
String s = "text/plain;twelve=\"1\\\"2\"";
MimeType mimeType = MimeType.valueOf(s);
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("text");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("plain");
assertThat(mimeType.getParameter("twelve")).isEqualTo("\"1\\\"2\"");
}
@Test
void withConversionService() {
ConversionService conversionService = new DefaultConversionService();
@@ -100,20 +100,6 @@ public interface WebSession {
*/
boolean isStarted();
/**
* Generate a new id for the session and update the underlying session
* storage to reflect the new id. After a successful call {@link #getId()}
* reflects the new session id.
* @return completion notification (success or error)
*/
Mono<Void> changeSessionId();
/**
* Invalidate the current session and clear session storage.
* @return completion notification (success or error)
*/
Mono<Void> invalidate();
/**
* Save the session through the {@code WebSessionStore} as follows:
* <ul>
@@ -131,6 +117,20 @@ public interface WebSession {
*/
Mono<Void> save();
/**
* Generate a new id for the session and update the underlying session
* storage to reflect the new id. After a successful call {@link #getId()}
* reflects the new session id.
* @return completion notification (success or error)
*/
Mono<Void> changeSessionId();
/**
* Invalidate the current session and clear session storage.
* @return completion notification (success or error)
*/
Mono<Void> invalidate();
/**
* Return {@code true} if the session expired after {@link #getMaxIdleTime()
* maxIdleTime} elapsed.
@@ -207,6 +207,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
private final AtomicReference<State> state = new AtomicReference<>(State.NEW);
private final Lock lock = new ReentrantLock();
public InMemoryWebSession(Instant creationTime, Duration maxIdleTime) {
this.creationTime = creationTime;
@@ -256,29 +257,6 @@ public class InMemoryWebSessionStore implements WebSessionStore {
return this.state.get().equals(State.STARTED) || !getAttributes().isEmpty();
}
@Override
public Mono<Void> changeSessionId() {
return Mono.<Void>defer(() -> {
String currentId = this.id.get();
InMemoryWebSessionStore.this.sessions.remove(currentId);
String newId = String.valueOf(idGenerator.generateId());
this.id.set(newId);
InMemoryWebSessionStore.this.sessions.put(this.id.get(), this);
return Mono.empty();
})
.subscribeOn(Schedulers.boundedElastic())
.publishOn(Schedulers.parallel())
.then();
}
@Override
public Mono<Void> invalidate() {
this.state.set(State.EXPIRED);
getAttributes().clear();
InMemoryWebSessionStore.this.sessions.remove(this.id.get());
return Mono.empty();
}
@Override
@SuppressWarnings("NullAway") // Dataflow analysis limitation
public Mono<Void> save() {
@@ -292,11 +270,19 @@ public class InMemoryWebSessionStore implements WebSessionStore {
if (isStarted()) {
// Save
InMemoryWebSessionStore.this.sessions.put(this.id.get(), this);
if (InMemoryWebSessionStore.this.sessions.get(getId()) == null) {
this.lock.lock();
try {
InMemoryWebSessionStore.this.sessions.putIfAbsent(getId(), this);
}
finally {
this.lock.unlock();
}
}
// Unless it was invalidated
if (this.state.get().equals(State.EXPIRED)) {
InMemoryWebSessionStore.this.sessions.remove(this.id.get());
InMemoryWebSessionStore.this.sessions.remove(getId());
return Mono.error(new IllegalStateException("Session was invalidated"));
}
}
@@ -307,12 +293,41 @@ public class InMemoryWebSessionStore implements WebSessionStore {
private void checkMaxSessionsLimit() {
if (sessions.size() >= maxSessions) {
expiredSessionChecker.removeExpiredSessions(clock.instant());
if (sessions.size() >= maxSessions && !sessions.containsKey(this.id.get())) {
if (sessions.size() >= maxSessions && !sessions.containsKey(getId())) {
throw new IllegalStateException("Max sessions limit reached: " + sessions.size());
}
}
}
@Override
public Mono<Void> changeSessionId() {
return Mono.<Void>defer(() -> {
this.lock.lock();
try {
String oldId = getId();
String newId = String.valueOf(idGenerator.generateId());
InMemoryWebSessionStore.this.sessions.remove(oldId);
InMemoryWebSessionStore.this.sessions.put(newId, this);
this.id.set(newId);
}
finally {
this.lock.unlock();
}
return Mono.empty();
})
.subscribeOn(Schedulers.boundedElastic())
.publishOn(Schedulers.parallel())
.then();
}
@Override
public Mono<Void> invalidate() {
this.state.set(State.EXPIRED);
getAttributes().clear();
InMemoryWebSessionStore.this.sessions.remove(getId());
return Mono.empty();
}
@Override
public boolean isExpired() {
return isExpired(clock.instant());
+1 -1
View File
@@ -61,7 +61,7 @@ dependencies {
testRuntimeOnly("org.glassfish:jakarta.el")
testRuntimeOnly("org.jruby:jruby")
testRuntimeOnly("org.python:jython-standalone")
testRuntimeOnly("org.webjars:underscorejs")
testRuntimeOnly("org.webjars:momentjs:2.29.4")
}
test {
@@ -90,7 +90,8 @@ public class LiteWebJarsResourceResolver extends AbstractResourceResolver {
.switchIfEmpty(Mono.defer(() -> {
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
if (webJarResourcePath != null) {
return chain.resolveUrlPath(webJarResourcePath, locations);
Mono<String> fallback = (webJarResourcePath.endsWith("/")) ? Mono.just(webJarResourcePath) : Mono.empty();
return chain.resolveUrlPath(webJarResourcePath, locations).switchIfEmpty(fallback);
}
else {
return Mono.empty();
@@ -29,6 +29,8 @@ import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRe
import org.springframework.web.testfixture.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -80,8 +82,8 @@ class LiteWebJarsResourceResolverTests {
@Test
void resolveUrlWebJarResource() {
String file = "underscorejs/underscore.js";
String expected = "underscorejs/1.8.3/underscore.js";
String file = "momentjs/momentjs.js";
String expected = "momentjs/2.29.4/momentjs.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.just(expected));
@@ -92,10 +94,24 @@ class LiteWebJarsResourceResolverTests {
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
}
@Test
void resolveUrlWebJarDirectory() {
String folder = "momentjs/locale/";
String expected = "momentjs/2.29.4/locale/";
given(this.chain.resolveUrlPath(folder, this.locations)).willReturn(Mono.empty());
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.empty());
String actual = this.resolver.resolveUrlPath(folder, this.locations, this.chain).block(TIMEOUT);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveUrlPath(folder, this.locations);
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
}
@Test
void resolveUrlWebJarResourceNotFound() {
String file = "something/something.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
String file = "momentjs/locale/unknown.js";
given(this.chain.resolveUrlPath(anyString(), eq(this.locations))).willReturn(Mono.empty());
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
@@ -134,11 +150,11 @@ class LiteWebJarsResourceResolverTests {
@Test
void resolveResourceWebJar() {
String file = "underscorejs/underscore.js";
String file = "momentjs/momentjs.js";
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.empty());
Resource expected = mock();
String expectedPath = "underscorejs/1.8.3/underscore.js";
String expectedPath = "momentjs/2.29.4/momentjs.js";
given(this.chain.resolveResource(this.exchange, expectedPath, this.locations))
.willReturn(Mono.just(expected));
+1 -1
View File
@@ -80,5 +80,5 @@ dependencies {
testRuntimeOnly("org.glassfish:jakarta.el")
testRuntimeOnly("org.jruby:jruby")
testRuntimeOnly("org.python:jython-standalone")
testRuntimeOnly("org.webjars:underscorejs")
testRuntimeOnly("org.webjars:momentjs:2.29.4")
}
@@ -88,7 +88,10 @@ public class LiteWebJarsResourceResolver extends AbstractResourceResolver {
if (path == null) {
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
if (webJarResourcePath != null) {
return chain.resolveUrlPath(webJarResourcePath, locations);
path = chain.resolveUrlPath(webJarResourcePath, locations);
if (path == null && webJarResourcePath.endsWith("/")) {
path = webJarResourcePath;
}
}
}
return path;
@@ -26,6 +26,8 @@ import org.springframework.core.io.Resource;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -74,8 +76,8 @@ class LiteWebJarsResourceResolverTests {
@Test
void resolveUrlWebJarResource() {
String file = "underscorejs/underscore.js";
String expected = "underscorejs/1.8.3/underscore.js";
String file = "momentjs/momentjs.js";
String expected = "momentjs/2.29.4/momentjs.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(expected);
@@ -86,10 +88,24 @@ class LiteWebJarsResourceResolverTests {
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
}
@Test
void resolveUrlWebJarDirectory() {
String folder = "momentjs/locale/";
String expected = "momentjs/2.29.4/locale/";
given(this.chain.resolveUrlPath(folder, this.locations)).willReturn(null);
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(null);
String actual = this.resolver.resolveUrlPath(folder, this.locations, this.chain);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveUrlPath(folder, this.locations);
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
}
@Test
void resolveUrlWebJarResourceNotFound() {
String file = "something/something.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
String file = "momentjs/locale/unknown.js";
given(this.chain.resolveUrlPath(anyString(), eq(this.locations))).willReturn(null);
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
@@ -125,8 +141,8 @@ class LiteWebJarsResourceResolverTests {
@Test
void resolveResourceWebJar() {
Resource expected = mock();
String file = "underscorejs/underscore.js";
String expectedPath = "underscorejs/1.8.3/underscore.js";
String file = "momentjs/momentjs.js";
String expectedPath = "momentjs/2.29.4/momentjs.js";
given(this.chain.resolveResource(this.request, expectedPath, this.locations)).willReturn(expected);
Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);
@@ -24,9 +24,9 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Assert;
import org.springframework.util.IdGenerator;
import org.springframework.util.JdkIdGenerator;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PingMessage;
@@ -44,7 +44,7 @@ import org.springframework.web.socket.WebSocketSession;
*/
public abstract class AbstractWebSocketSession<T> implements NativeWebSocketSession {
protected static final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
protected static final IdGenerator idGenerator = new JdkIdGenerator();
protected static final Log logger = LogFactory.getLog(NativeWebSocketSession.class);