diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java index f6e3088ac3d..f29594a4339 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -235,6 +235,24 @@ class EndpointRequestTests { assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false"); } + @Test + void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root"); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("/", () -> List + .of(mockEndpoint(EndpointId.of("root"), "/"), mockEndpoint(EndpointId.of("alpha"), "alpha")))); + assertMatcher.doesNotMatch("/"); + assertMatcher.matches("/alpha"); + assertMatcher.matches("/alpha/sub"); + } + + @Test + void toEndpointWhenEndpointPathMappedToRootShouldMatchRoot() { + ServerWebExchangeMatcher matcher = EndpointRequest.to("root"); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, + new PathMappedEndpoints("/", () -> List.of(mockEndpoint(EndpointId.of("root"), "/")))); + assertMatcher.matches("/"); + } + private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) { return assertMatcher(matcher, mockPathMappedEndpoints("/actuator")); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java index b172485e881..440b484b830 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -24,6 +24,7 @@ import org.assertj.core.api.AssertDelegateTarget; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.EndpointRequestMatcher; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.Operation; @@ -239,6 +240,24 @@ class EndpointRequestTests { assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false"); } + @Test + void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() { + EndpointRequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root"); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", () -> List + .of(mockEndpoint(EndpointId.of("root"), "/"), mockEndpoint(EndpointId.of("alpha"), "alpha")))); + assertMatcher.doesNotMatch("/"); + assertMatcher.matches("/alpha"); + assertMatcher.matches("/alpha/sub"); + } + + @Test + void toEndpointWhenEndpointPathMappedToRootShouldMatchRoot() { + EndpointRequestMatcher matcher = EndpointRequest.to("root"); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, + new PathMappedEndpoints("", () -> List.of(mockEndpoint(EndpointId.of("root"), "/")))); + assertMatcher.matches("/"); + } + private RequestMatcherAssert assertMatcher(RequestMatcher matcher) { return assertMatcher(matcher, mockPathMappedEndpoints("/actuator")); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java index d9a0ee51906..c8be88751b1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-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. @@ -140,7 +140,17 @@ public class PathMappedEndpoints implements Iterable { } private String getPath(PathMappedEndpoint endpoint) { - return (endpoint != null) ? this.basePath + "/" + endpoint.getRootPath() : null; + if (endpoint == null) { + return null; + } + StringBuilder path = new StringBuilder(this.basePath); + if (!this.basePath.equals("/")) { + path.append("/"); + } + if (!endpoint.getRootPath().equals("/")) { + path.append(endpoint.getRootPath()); + } + return path.toString(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java index badfac2add3..1f9557bc8e0 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -86,7 +86,7 @@ class RequestPredicateFactory { boolean matchRemainingPathSegments) { StringBuilder path = new StringBuilder(rootPath); for (int i = 0; i < selectorParameters.length; i++) { - path.append("/{"); + path.append((i != 0 || !rootPath.endsWith("/")) ? "/{" : "{"); if (i == selectorParameters.length - 1 && matchRemainingPathSegments) { path.append("*"); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java index fc1d38061ee..3621a5b94a5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -19,8 +19,10 @@ package org.springframework.boot.actuate.endpoint.web.reactive; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.Principal; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -176,10 +178,19 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi private RequestMappingInfo createRequestMappingInfo(WebOperation operation) { WebOperationRequestPredicate predicate = operation.getRequestPredicate(); String path = this.endpointMapping.createSubPath(predicate.getPath()); + List paths = new ArrayList<>(); + paths.add(path); + if (!StringUtils.hasText(path)) { + paths.add("/"); + } RequestMethod method = RequestMethod.valueOf(predicate.getHttpMethod().name()); String[] consumes = StringUtils.toStringArray(predicate.getConsumes()); String[] produces = StringUtils.toStringArray(predicate.getProduces()); - return RequestMappingInfo.paths(path).methods(method).consumes(consumes).produces(produces).build(); + return RequestMappingInfo.paths(paths.toArray(new String[0])) + .methods(method) + .consumes(consumes) + .produces(produces) + .build(); } private void registerLinksMapping() { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java index 8f1afcc02e0..fefac91cd87 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java @@ -196,7 +196,13 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin } private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) { - return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path)) + String subPath = this.endpointMapping.createSubPath(path); + List paths = new ArrayList<>(); + paths.add(subPath); + if (!StringUtils.hasLength(subPath)) { + paths.add("/"); + } + return RequestMappingInfo.paths(paths.toArray(new String[0])) .options(this.builderConfig) .methods(RequestMethod.valueOf(predicate.getHttpMethod().name())) .consumes(predicate.getConsumes().toArray(new String[0])) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java index 246a3991caf..37e3596fdfb 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -91,6 +91,20 @@ class PathMappedEndpointsTests { assertThat(mapped.getPath(EndpointId.of("xx"))).isNull(); } + @Test + void getPathWhenBasePathIsRootAndEndpointIsPathMappedToRootShouldReturnSingleSlash() { + PathMappedEndpoints mapped = new PathMappedEndpoints("/", + () -> List.of(mockEndpoint(EndpointId.of("root"), "/"))); + assertThat(mapped.getPath(EndpointId.of("root"))).isEqualTo("/"); + } + + @Test + void getPathWhenBasePathIsRootAndEndpointIsPathMapped() { + PathMappedEndpoints mapped = new PathMappedEndpoints("/", + () -> List.of(mockEndpoint(EndpointId.of("a"), "alpha"))); + assertThat(mapped.getPath(EndpointId.of("a"))).isEqualTo("/alpha"); + } + @Test void getAllRootPathsShouldReturnAllPaths() { PathMappedEndpoints mapped = createTestMapped(null); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java index d23d6fe380d..23f370bac33 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -38,6 +38,7 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; +import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -108,6 +109,21 @@ public abstract class AbstractWebEndpointIntegrationTests { + client.get().uri("/").exchange().expectStatus().isOk().expectBody().jsonPath("All").isEqualTo(true); + client.get() + .uri("/some-part") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("part") + .isEqualTo("some-part"); + }); + } + @Test void readOperationWithSelector() { load(TestEndpointConfiguration.class, @@ -672,6 +688,17 @@ public abstract class AbstractWebEndpointIntegrationTests "/"; + } + + } + @Configuration(proxyBeanMethods = false) @Import(BaseConfiguration.class) static class MatchAllRemainingEndpointConfiguration { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java index 9b8b8094db9..6645dc5b64a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -20,9 +20,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -62,11 +64,11 @@ class BaseConfiguration { @Bean WebEndpointDiscoverer webEndpointDiscoverer(EndpointMediaTypes endpointMediaTypes, - ApplicationContext applicationContext) { + ApplicationContext applicationContext, ObjectProvider pathMappers) { ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper( DefaultConversionService.getSharedInstance()); - return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, - Collections.emptyList(), Collections.emptyList()); + return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, + pathMappers.orderedStream().toList(), Collections.emptyList(), Collections.emptyList()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java index 372a8a61b84..3c8ea7d78e2 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -68,6 +68,13 @@ class RequestPredicateFactoryTests { assertThat(requestPredicate.getPath()).isEqualTo("/root/{one}/{*two}"); } + @Test + void getRequestPredicateWithSlashRootReturnsPredicateWithPathWithoutDoubleSlash() { + DiscoveredOperationMethod operationMethod = getDiscoveredOperationMethod(ValidSelectors.class); + WebOperationRequestPredicate requestPredicate = this.factory.getRequestPredicate("/", operationMethod); + assertThat(requestPredicate.getPath()).isEqualTo("/{one}/{*two}"); + } + private DiscoveredOperationMethod getDiscoveredOperationMethod(Class source) { Method method = source.getDeclaredMethods()[0]; AnnotationAttributes attributes = new AnnotationAttributes();