From 76b2d13b2cd2adc9b0ccf6461bbe7e15532cd04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 6 Aug 2024 10:26:04 +0200 Subject: [PATCH] Add support for changing context path in ServletRequestPath This commit implements modifyContextPath in ServletRequestPath and apply the same logic of concatenating the servlet path with the context path. Closes gh-33251 --- .../web/util/ServletRequestPathUtils.java | 49 ++++++++++++++--- .../util/ServletRequestPathUtilsTests.java | 55 +++++++++++++++++-- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java index 86b34c77b49..4c627896589 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ import org.springframework.util.StringUtils; * {@link org.springframework.util.PathMatcher} otherwise. * * @author Rossen Stoyanchev + * @author Stephane Nicoll * @since 5.3 */ public abstract class ServletRequestPathUtils { @@ -186,14 +187,16 @@ public abstract class ServletRequestPathUtils { */ private static final class ServletRequestPath implements RequestPath { + private final PathElements pathElements; + private final RequestPath requestPath; private final PathContainer contextPath; - private ServletRequestPath(String rawPath, @Nullable String contextPath, String servletPathPrefix) { - Assert.notNull(servletPathPrefix, "`servletPathPrefix` is required"); - this.requestPath = RequestPath.parse(rawPath, contextPath + servletPathPrefix); - this.contextPath = PathContainer.parsePath(StringUtils.hasText(contextPath) ? contextPath : ""); + private ServletRequestPath(PathElements pathElements) { + this.pathElements = pathElements; + this.requestPath = pathElements.createRequestPath(); + this.contextPath = pathElements.createContextPath(); } @Override @@ -218,7 +221,7 @@ public abstract class ServletRequestPathUtils { @Override public RequestPath modifyContextPath(String contextPath) { - throw new UnsupportedOperationException(); + return new ServletRequestPath(this.pathElements.withContextPath(contextPath)); } @@ -249,7 +252,7 @@ public abstract class ServletRequestPathUtils { requestUri = (requestUri != null ? requestUri : request.getRequestURI()); String servletPathPrefix = getServletPathPrefix(request); return (StringUtils.hasText(servletPathPrefix) ? - new ServletRequestPath(requestUri, request.getContextPath(), servletPathPrefix) : + new ServletRequestPath(new PathElements(requestUri, request.getContextPath(), servletPathPrefix)) : RequestPath.parse(requestUri, request.getContextPath())); } @@ -265,6 +268,38 @@ public abstract class ServletRequestPathUtils { } return null; } + + record PathElements(String rawPath, @Nullable String contextPath, String servletPathPrefix) { + + PathElements { + Assert.notNull(servletPathPrefix, "`servletPathPrefix` is required"); + } + + private RequestPath createRequestPath() { + return RequestPath.parse(this.rawPath, this.contextPath + this.servletPathPrefix); + } + + private PathContainer createContextPath() { + return PathContainer.parsePath(StringUtils.hasText(this.contextPath) ? this.contextPath : ""); + } + + PathElements withContextPath(String contextPath) { + if (!contextPath.startsWith("/") || contextPath.endsWith("/")) { + throw new IllegalArgumentException("Invalid contextPath '" + contextPath + "': " + + "must start with '/' and not end with '/'"); + } + String contextPathToUse = this.servletPathPrefix + contextPath; + if (StringUtils.hasText(this.contextPath())) { + throw new IllegalStateException("Could not change context path to '" + contextPathToUse + + "': a context path is already specified"); + } + if (!this.rawPath.startsWith(contextPathToUse)) { + throw new IllegalArgumentException("Invalid contextPath '" + contextPathToUse + "': " + + "must match the start of requestPath: '" + this.rawPath + "'"); + } + return new PathElements(this.rawPath, contextPathToUse, ""); + } + } } } diff --git a/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java index aa2e2803726..90a06875f75 100644 --- a/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/ServletRequestPathUtilsTests.java @@ -24,11 +24,14 @@ import org.springframework.web.testfixture.servlet.MockHttpServletMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ServletRequestPathUtils}. * * @author Rossen Stoyanchev + * @author Stephane Nicoll */ class ServletRequestPathUtilsTests { @@ -47,19 +50,63 @@ class ServletRequestPathUtilsTests { testParseAndCache("/app/servlet/a//", "/app", "/servlet", "/a//"); } + @Test + void modifyPathContextWithExistingContextPath() { + RequestPath requestPath = createRequestPath("/app/api/persons/42", "/app", "/api", "/persons/42"); + assertThatIllegalStateException().isThrownBy(() -> requestPath.modifyContextPath("/persons")) + .withMessage("Could not change context path to '/api/persons': a context path is already specified"); + } + + @Test + void modifyPathContextWhenContextPathIsNotInThePath() { + RequestPath requestPath = createRequestPath("/api/persons/42", "", "/api", "/persons/42"); + assertThatIllegalArgumentException().isThrownBy(() -> requestPath.modifyContextPath("/something")) + .withMessage("Invalid contextPath '/api/something': " + + "must match the start of requestPath: '/api/persons/42'"); + } + + @Test + void modifyPathContextReplacesServletPath() { + RequestPath requestPath = createRequestPath("/api/persons/42", "", "/api", "/persons/42"); + RequestPath updatedRequestPath = requestPath.modifyContextPath("/persons"); + assertThat(updatedRequestPath.contextPath().value()).isEqualTo("/api/persons"); + assertThat(updatedRequestPath.pathWithinApplication().value()).isEqualTo("/42"); + assertThat(updatedRequestPath.value()).isEqualTo("/api/persons/42"); + } + + @Test + void modifyPathContextWithContextPathNotStartingWithSlash() { + RequestPath requestPath = createRequestPath("/api/persons/42", "", "/api", "/persons/42"); + assertThatIllegalArgumentException().isThrownBy(() -> requestPath.modifyContextPath("persons")) + .withMessage("Invalid contextPath 'persons': must start with '/' and not end with '/'"); + } + + @Test + void modifyPathContextWithContextPathEndingWithSlash() { + RequestPath requestPath = createRequestPath("/api/persons/42", "", "/api", "/persons/42"); + assertThatIllegalArgumentException().isThrownBy(() -> requestPath.modifyContextPath("/persons/")) + .withMessage("Invalid contextPath '/persons/': must start with '/' and not end with '/'"); + } + private void testParseAndCache( String requestUri, String contextPath, String servletPath, String pathWithinApplication) { + RequestPath requestPath = createRequestPath(requestUri, contextPath, servletPath, pathWithinApplication); + + assertThat(requestPath.contextPath().value()).isEqualTo(contextPath); + assertThat(requestPath.pathWithinApplication().value()).isEqualTo(pathWithinApplication); + } + + private static RequestPath createRequestPath( + String requestUri, String contextPath, String servletPath, String pathWithinApplication) { + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setContextPath(contextPath); request.setServletPath(servletPath); request.setHttpServletMapping(new MockHttpServletMapping( pathWithinApplication, contextPath, "myServlet", MappingMatch.PATH)); - RequestPath requestPath = ServletRequestPathUtils.parseAndCache(request); - - assertThat(requestPath.contextPath().value()).isEqualTo(contextPath); - assertThat(requestPath.pathWithinApplication().value()).isEqualTo(pathWithinApplication); + return ServletRequestPathUtils.parseAndCache(request); } }