From ba93e6c0ed54c96aec485993986386d5f7dd12ff Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Aug 2022 15:45:20 +0100 Subject: [PATCH] Restore support for Jersey Closes gh-28637 --- .../DocumentConfigurationProperties.java | 3 +- .../build.gradle | 4 + ...ndpointManagementContextConfiguration.java | 20 +- ...ndpointManagementContextConfiguration.java | 196 ++++++++++ .../endpoint/web/jersey/package-info.java | 20 + ...althEndpointWebExtensionConfiguration.java | 94 +++++ .../JerseyServerMetricsAutoConfiguration.java | 105 ++++++ .../metrics/jersey/package-info.java | 20 + ...atchersManagementContextConfiguration.java | 19 +- ...eyChildManagementContextConfiguration.java | 58 +++ .../JerseyManagementContextConfiguration.java | 42 +++ ...seySameManagementContextConfiguration.java | 77 ++++ ...gementContextResourceConfigCustomizer.java | 36 ++ .../web/jersey/package-info.java | 20 + ...web.ManagementContextConfiguration.imports | 3 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...ntManagementContextConfigurationTests.java | 24 +- .../JerseyWebEndpointIntegrationTests.java | 75 ++++ ...ntManagementContextConfigurationTests.java | 67 ++++ .../JerseyEndpointIntegrationTests.java | 159 ++++++++ ...ndpointAdditionalPathIntegrationTests.java | 56 +++ ...eyServerMetricsAutoConfigurationTests.java | 160 ++++++++ ...stractEndpointRequestIntegrationTests.java | 8 +- ...JerseyEndpointRequestIntegrationTests.java | 130 +++++++ ...rsManagementContextConfigurationTests.java | 38 +- ...ldManagementContextConfigurationTests.java | 106 ++++++ ...meManagementContextConfigurationTests.java | 136 +++++++ ...ntextConfigurationImportSelectorTests.java | 4 + .../spring-boot-actuator/build.gradle | 4 + .../jersey/JerseyEndpointResourceFactory.java | 356 ++++++++++++++++++ ...EndpointAdditionalPathResourceFactory.java | 66 ++++ .../JerseyRemainingPathSegmentProvider.java | 31 ++ .../endpoint/web/jersey/package-info.java | 20 + .../AbstractWebEndpointIntegrationTests.java | 2 +- .../JerseyWebEndpointIntegrationTests.java | 171 +++++++++ ...EndpointTestInvocationContextProvider.java | 55 +++ .../spring-boot-autoconfigure/build.gradle | 6 + .../jersey/JerseyAutoConfiguration.java | 232 ++++++++++++ .../jersey/JerseyProperties.java | 127 +++++++ .../jersey/ResourceConfigCustomizer.java | 37 ++ .../autoconfigure/jersey/package-info.java | 20 + .../servlet/DefaultJerseyApplicationPath.java | 60 +++ .../web/servlet/JerseyApplicationPath.java | 90 +++++ ...itional-spring-configuration-metadata.json | 4 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...toConfigurationCustomApplicationTests.java | 98 +++++ ...igurationCustomFilterContextPathTests.java | 100 +++++ ...utoConfigurationCustomFilterPathTests.java | 99 +++++ ...ConfigurationCustomLoadOnStartupTests.java | 76 ++++ ...rationCustomObjectMapperProviderTests.java | 126 +++++++ ...gurationCustomServletContextPathTests.java | 98 +++++ ...toConfigurationCustomServletPathTests.java | 98 +++++ ...toConfigurationDefaultFilterPathTests.java | 97 +++++ ...oConfigurationDefaultServletPathTests.java | 96 +++++ ...onfigurationObjectMapperProviderTests.java | 136 +++++++ ...utoConfigurationServletContainerTests.java | 111 ++++++ .../jersey/JerseyAutoConfigurationTests.java | 130 +++++++ ...figurationWithoutApplicationPathTests.java | 96 +++++ .../servlet/JerseyApplicationPathTests.java | 83 ++++ .../spring-boot-dependencies/build.gradle | 8 + .../spring-boot-docs/build.gradle | 2 + .../src/docs/asciidoc/actuator/endpoints.adoc | 12 +- .../src/docs/asciidoc/actuator/metrics.adoc | 35 ++ .../docs/asciidoc/actuator/monitoring.adoc | 3 +- .../docs/asciidoc/anchor-rewrite.properties | 6 + .../src/docs/asciidoc/documentation/web.adoc | 2 +- .../src/docs/asciidoc/howto.adoc | 2 + .../src/docs/asciidoc/howto/jersey.adoc | 30 ++ .../src/docs/asciidoc/web/servlet.adoc | 44 ++- .../Endpoint.java | 21 ++ .../JerseyConfig.java | 32 ++ .../howto/jersey/springsecurity/Endpoint.java | 21 ++ .../JerseySetStatusOverSendErrorConfig.java | 33 ++ .../docs/web/servlet/jersey/MyEndpoint.java | 33 ++ .../web/servlet/jersey/MyJerseyConfig.java | 30 ++ .../spring-boot-starter-jersey/build.gradle | 26 ++ .../SpringBootTestContextBootstrapper.java | 5 +- .../boot/WebApplicationType.java | 7 +- .../build.gradle | 18 + .../main/java/smoketest/jersey/Endpoint.java | 39 ++ .../java/smoketest/jersey/JerseyConfig.java | 31 ++ .../smoketest/jersey/ReverseEndpoint.java | 35 ++ .../jersey/SampleJerseyApplication.java | 30 ++ .../main/java/smoketest/jersey/Service.java | 32 ++ .../AbstractJerseyApplicationTests.java | 62 +++ .../AbstractJerseyManagementPortTests.java | 112 ++++++ ...ApplicationPathAndManagementPortTests.java | 58 +++ ...entPortSampleActuatorApplicationTests.java | 54 +++ .../jersey/JerseyFilterApplicationTests.java | 29 ++ .../JerseyFilterManagementPortTests.java | 30 ++ .../jersey/JerseyServletApplicationTests.java | 26 ++ .../JerseyServletManagementPortTests.java | 27 ++ .../build.gradle | 14 + .../smoketest/secure/jersey/Endpoint.java | 39 ++ .../smoketest/secure/jersey/JerseyConfig.java | 31 ++ .../secure/jersey/ReverseEndpoint.java | 35 ++ .../jersey/SampleSecureJerseyApplication.java | 63 ++++ .../secure/jersey/SecurityConfiguration.java | 54 +++ .../java/smoketest/secure/jersey/Service.java | 32 ++ .../src/main/resources/application.properties | 4 + .../jersey/AbstractJerseySecureTests.java | 161 ++++++++ .../CustomApplicationPathActuatorTests.java | 45 +++ .../jersey/JerseySecureApplicationTests.java | 44 +++ ...mentPortAndPathJerseyApplicationTests.java | 65 ++++ ...tPortCustomApplicationPathJerseyTests.java | 63 ++++ 105 files changed, 5974 insertions(+), 18 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/ManagementContextResourceConfigCustomizer.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyHealthEndpointAdditionalPathIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyRemainingPathSegmentProvider.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/ResourceConfigCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomApplicationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomLoadOnStartupTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletContextPathTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletPathTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultServletPathTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationServletContainerTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationWithoutApplicationPathTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPathTests.java create mode 100644 spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/jersey.adoc create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyEndpoint.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyJerseyConfig.java create mode 100644 spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Endpoint.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/JerseyConfig.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/ReverseEndpoint.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Service.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterManagementPortTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletManagementPortTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index 221586300a..3a1c970bbf 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -177,10 +177,11 @@ public class DocumentConfigurationProperties extends DefaultTask { prefix.accept("spring.graphql"); prefix.accept("spring.hateoas"); prefix.accept("spring.http"); - prefix.accept("spring.servlet"); + prefix.accept("spring.jersey"); prefix.accept("spring.mvc"); prefix.accept("spring.netty"); prefix.accept("spring.resources"); + prefix.accept("spring.servlet"); prefix.accept("spring.session"); prefix.accept("spring.web"); prefix.accept("spring.webflux"); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index b304ebf8e3..bc017df983 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -107,6 +107,8 @@ dependencies { exclude group: "commons-logging", module: "commons-logging" } optional("org.flywaydb:flyway-core") + optional("org.glassfish.jersey.core:jersey-server") + optional("org.glassfish.jersey.containers:jersey-container-servlet-core") optional("org.hibernate.orm:hibernate-core") optional("org.hibernate.orm:hibernate-micrometer") optional("org.hibernate.validator:hibernate-validator") @@ -168,6 +170,8 @@ dependencies { testImplementation("org.eclipse.jetty:jetty-webapp") { exclude group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api" } + testImplementation("org.glassfish.jersey.ext:jersey-spring6") + testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson") testImplementation("org.hamcrest:hamcrest") testImplementation("org.hsqldb:hsqldb") testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java index 6251dfbcf8..16e6453a1c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -16,15 +16,19 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web; +import org.glassfish.jersey.server.ResourceConfig; + import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.endpoint.web.ExposableServletEndpoint; import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar; import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.DispatcherServlet; @@ -63,4 +67,18 @@ public class ServletEndpointManagementContextConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ResourceConfig.class) + @ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") + public static class JerseyServletEndpointManagementContextConfiguration { + + @Bean + public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties, + ServletEndpointsSupplier servletEndpointsSupplier, JerseyApplicationPath jerseyApplicationPath) { + return new ServletEndpointRegistrar(jerseyApplicationPath.getRelativePath(properties.getBasePath()), + servletEndpointsSupplier.getEndpoints()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java new file mode 100644 index 0000000000..5bac6e79a9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -0,0 +1,196 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.jersey.ManagementContextResourceConfigCustomizer; +import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableServletEndpoint; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; +import org.springframework.boot.actuate.endpoint.web.jersey.JerseyHealthEndpointAdditionalPathResourceFactory; +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +/** + * {@link ManagementContextConfiguration @ManagementContextConfiguration} for Jersey + * {@link Endpoint @Endpoint} concerns. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @author Michael Simons + * @author Madhura Bhave + * @author HaiTao Zhang + */ +@ManagementContextConfiguration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass(ResourceConfig.class) +@ConditionalOnBean(WebEndpointsSupplier.class) +@ConditionalOnMissingBean(type = "org.springframework.web.servlet.DispatcherServlet") +class JerseyWebEndpointManagementContextConfiguration { + + private static final EndpointId HEALTH_ENDPOINT_ID = EndpointId.of("health"); + + @Bean + JerseyWebEndpointsResourcesRegistrar jerseyWebEndpointsResourcesRegistrar(Environment environment, + WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, + EndpointMediaTypes endpointMediaTypes, WebEndpointProperties webEndpointProperties) { + String basePath = webEndpointProperties.getBasePath(); + boolean shouldRegisterLinks = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath); + return new JerseyWebEndpointsResourcesRegistrar(webEndpointsSupplier, servletEndpointsSupplier, + endpointMediaTypes, basePath, shouldRegisterLinks); + } + + @Bean + @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + @ConditionalOnBean(HealthEndpoint.class) + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) + JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentPortAdditionalHealthEndpointPathsResourcesRegistrar( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) { + Collection webEndpoints = webEndpointsSupplier.getEndpoints(); + ExposableWebEndpoint health = webEndpoints.stream() + .filter((endpoint) -> endpoint.getEndpointId().equals(HEALTH_ENDPOINT_ID)).findFirst().get(); + return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups); + } + + private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment, + String basePath) { + return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) + || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); + } + + /** + * Register endpoints with the {@link ResourceConfig} for the management context. + */ + static class JerseyWebEndpointsResourcesRegistrar implements ManagementContextResourceConfigCustomizer { + + private final WebEndpointsSupplier webEndpointsSupplier; + + private final ServletEndpointsSupplier servletEndpointsSupplier; + + private final EndpointMediaTypes mediaTypes; + + private final String basePath; + + private final boolean shouldRegisterLinks; + + JerseyWebEndpointsResourcesRegistrar(WebEndpointsSupplier webEndpointsSupplier, + ServletEndpointsSupplier servletEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, + String basePath, boolean shouldRegisterLinks) { + this.webEndpointsSupplier = webEndpointsSupplier; + this.servletEndpointsSupplier = servletEndpointsSupplier; + this.mediaTypes = endpointMediaTypes; + this.basePath = basePath; + this.shouldRegisterLinks = shouldRegisterLinks; + } + + @Override + public void customize(ResourceConfig config) { + register(config); + } + + private void register(ResourceConfig config) { + Collection webEndpoints = this.webEndpointsSupplier.getEndpoints(); + Collection servletEndpoints = this.servletEndpointsSupplier.getEndpoints(); + EndpointLinksResolver linksResolver = getLinksResolver(webEndpoints, servletEndpoints); + EndpointMapping mapping = new EndpointMapping(this.basePath); + Collection endpointResources = new JerseyEndpointResourceFactory().createEndpointResources( + mapping, webEndpoints, this.mediaTypes, linksResolver, this.shouldRegisterLinks); + register(endpointResources, config); + } + + private EndpointLinksResolver getLinksResolver(Collection webEndpoints, + Collection servletEndpoints) { + List> endpoints = new ArrayList<>(webEndpoints.size() + servletEndpoints.size()); + endpoints.addAll(webEndpoints); + endpoints.addAll(servletEndpoints); + return new EndpointLinksResolver(endpoints, this.basePath); + } + + private void register(Collection resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); + } + + } + + class JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar + implements ManagementContextResourceConfigCustomizer { + + private final ExposableWebEndpoint endpoint; + + private final HealthEndpointGroups groups; + + JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(ExposableWebEndpoint endpoint, + HealthEndpointGroups groups) { + this.endpoint = endpoint; + this.groups = groups; + } + + @Override + public void customize(ResourceConfig config) { + register(config); + } + + private void register(ResourceConfig config) { + EndpointMapping mapping = new EndpointMapping(""); + JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( + WebServerNamespace.MANAGEMENT, this.groups); + Collection endpointResources = resourceFactory + .createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false) + .stream().filter(Objects::nonNull).collect(Collectors.toList()); + register(endpointResources, config); + } + + private void register(Collection resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/package-info.java new file mode 100644 index 0000000000..feeb816b35 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for exposing actuator web endpoints using Jersey. + */ +package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index d316d94003..857b628295 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -17,21 +17,40 @@ package org.springframework.boot.actuate.autoconfigure.health; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.stream.Collectors; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.servlet.ServletContainer; + +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; +import org.springframework.boot.actuate.endpoint.web.jersey.JerseyHealthEndpointAdditionalPathResourceFactory; import org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping; import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.jersey.JerseyProperties; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; +import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.DispatcherServlet; @@ -78,4 +97,79 @@ class HealthEndpointWebExtensionConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ResourceConfig.class) + @ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) + static class JerseyAdditionalHealthEndpointPathsConfiguration { + + @Bean + JerseyAdditionalHealthEndpointPathsResourcesRegistrar jerseyAdditionalHealthEndpointPathsResourcesRegistrar( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) { + ExposableWebEndpoint health = getHealthEndpoint(webEndpointsSupplier); + return new JerseyAdditionalHealthEndpointPathsResourcesRegistrar(health, healthEndpointGroups); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(ResourceConfig.class) + @EnableConfigurationProperties(JerseyProperties.class) + static class JerseyInfrastructureConfiguration { + + @Bean + @ConditionalOnMissingBean(JerseyApplicationPath.class) + JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties, ResourceConfig config) { + return new DefaultJerseyApplicationPath(properties.getApplicationPath(), config); + } + + @Bean + ResourceConfig resourceConfig(ObjectProvider resourceConfigCustomizers) { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfigCustomizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig)); + return resourceConfig; + } + + @Bean + ServletRegistrationBean jerseyServletRegistration( + JerseyApplicationPath jerseyApplicationPath, ResourceConfig resourceConfig) { + return new ServletRegistrationBean<>(new ServletContainer(resourceConfig), + jerseyApplicationPath.getUrlMapping()); + } + + } + + } + + static class JerseyAdditionalHealthEndpointPathsResourcesRegistrar implements ResourceConfigCustomizer { + + private final ExposableWebEndpoint endpoint; + + private final HealthEndpointGroups groups; + + JerseyAdditionalHealthEndpointPathsResourcesRegistrar(ExposableWebEndpoint endpoint, + HealthEndpointGroups groups) { + this.endpoint = endpoint; + this.groups = groups; + } + + @Override + public void customize(ResourceConfig config) { + register(config); + } + + private void register(ResourceConfig config) { + EndpointMapping mapping = new EndpointMapping(""); + JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( + WebServerNamespace.SERVER, this.groups); + Collection endpointResources = resourceFactory + .createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false) + .stream().filter(Objects::nonNull).collect(Collectors.toList()); + register(endpointResources, config); + } + + private void register(Collection resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java new file mode 100644 index 0000000000..382d71daf9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.jersey; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.jersey.server.AnnotationFinder; +import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider; +import io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider; +import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener; +import io.micrometer.core.instrument.config.MeterFilter; +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server; +import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Jersey server instrumentation. + * + * @author Michael Weirauch + * @author Michael Simons + * @author Andy Wilkinson + * @since 2.1.0 + */ +@AutoConfiguration(after = { MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass({ ResourceConfig.class, MetricsApplicationEventListener.class }) +@ConditionalOnBean({ MeterRegistry.class, ResourceConfig.class }) +@EnableConfigurationProperties(MetricsProperties.class) +public class JerseyServerMetricsAutoConfiguration { + + private final MetricsProperties properties; + + public JerseyServerMetricsAutoConfiguration(MetricsProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean(JerseyTagsProvider.class) + public DefaultJerseyTagsProvider jerseyTagsProvider() { + return new DefaultJerseyTagsProvider(); + } + + @Bean + public ResourceConfigCustomizer jerseyServerMetricsResourceConfigCustomizer(MeterRegistry meterRegistry, + JerseyTagsProvider tagsProvider) { + Server server = this.properties.getWeb().getServer(); + return (config) -> config.register( + new MetricsApplicationEventListener(meterRegistry, tagsProvider, server.getRequest().getMetricName(), + server.getRequest().getAutotime().isEnabled(), new AnnotationUtilsAnnotationFinder())); + } + + @Bean + @Order(0) + public MeterFilter jerseyMetricsUriTagFilter() { + String metricName = this.properties.getWeb().getServer().getRequest().getMetricName(); + MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter( + () -> String.format("Reached the maximum number of URI tags for '%s'.", metricName)); + return MeterFilter.maximumAllowableTags(metricName, "uri", this.properties.getWeb().getServer().getMaxUriTags(), + filter); + } + + /** + * An {@link AnnotationFinder} that uses {@link AnnotationUtils}. + */ + private static class AnnotationUtilsAnnotationFinder implements AnnotationFinder { + + @Override + public A findAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + return AnnotationUtils.findAnnotation(annotatedElement, annotationType); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/package-info.java new file mode 100644 index 0000000000..61cd95ee1a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Jersey actuator metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.jersey; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java index 53c06a7688..af67a52570 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -16,14 +16,18 @@ package org.springframework.boot.actuate.autoconfigure.security.servlet; +import org.glassfish.jersey.server.ResourceConfig; + import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider; import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -55,4 +59,17 @@ public class SecurityRequestMatchersManagementContextConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ResourceConfig.class) + @ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") + @ConditionalOnBean(JerseyApplicationPath.class) + public static class JerseyRequestMatcherConfiguration { + + @Bean + public RequestMatcherProvider requestMatcherProvider(JerseyApplicationPath applicationPath) { + return new AntPathRequestMatcherProvider(applicationPath::getRelativePath); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java new file mode 100644 index 0000000000..7c3f1bc91d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * {@link ManagementContextConfiguration @ManagementContextConfiguration} for Jersey + * infrastructure when a separate management context with a web server running on a + * different port is required. + * + * @author Madhura Bhave + * @since 2.1.0 + */ +@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false) +@Import(JerseyManagementContextConfiguration.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass(ResourceConfig.class) +@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") +public class JerseyChildManagementContextConfiguration { + + @Bean + public JerseyApplicationPath jerseyApplicationPath() { + return () -> "/"; + } + + @Bean + ResourceConfig resourceConfig(ObjectProvider customizers) { + ResourceConfig resourceConfig = new ResourceConfig(); + customizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig)); + return resourceConfig; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java new file mode 100644 index 0000000000..0c4e7389a9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.jersey; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; + +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Shared configuration for Jersey-based actuators regardless of management context type. + * + * @author Madhura Bhave + */ +@Configuration(proxyBeanMethods = false) +class JerseyManagementContextConfiguration { + + @Bean + ServletRegistrationBean jerseyServletRegistration(JerseyApplicationPath jerseyApplicationPath, + ResourceConfig resourceConfig) { + return new ServletRegistrationBean<>(new ServletContainer(resourceConfig), + jerseyApplicationPath.getUrlMapping()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java new file mode 100644 index 0000000000..9d8b01778e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.jersey.JerseyProperties; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; +import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link ManagementContextConfiguration @ManagementContextConfiguration} for Jersey + * infrastructure when the management context is the same as the main application context. + * + * @author Madhura Bhave + * @since 2.1.0 + */ +@ManagementContextConfiguration(value = ManagementContextType.SAME, proxyBeanMethods = false) +@EnableConfigurationProperties(JerseyProperties.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass(ResourceConfig.class) +@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") +public class JerseySameManagementContextConfiguration { + + @Bean + ResourceConfigCustomizer managementResourceConfigCustomizerAdapter( + ObjectProvider customizers) { + return (config) -> customizers.orderedStream().forEach((customizer) -> customizer.customize(config)); + } + + @Configuration(proxyBeanMethods = false) + @Import(JerseyManagementContextConfiguration.class) + @ConditionalOnMissingBean(ResourceConfig.class) + static class JerseyInfrastructureConfiguration { + + @Bean + @ConditionalOnMissingBean(JerseyApplicationPath.class) + JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties, ResourceConfig config) { + return new DefaultJerseyApplicationPath(properties.getApplicationPath(), config); + } + + @Bean + ResourceConfig resourceConfig(ObjectProvider resourceConfigCustomizers) { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfigCustomizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig)); + return resourceConfig; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/ManagementContextResourceConfigCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/ManagementContextResourceConfigCustomizer.java new file mode 100644 index 0000000000..14ae2ddbc9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/ManagementContextResourceConfigCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +/** + * Callback interface that can be implemented by beans wishing to customize Jersey's + * {@link ResourceConfig} in the management context before it is used. + * + * @author Andy Wilkinson + * @since 2.3.10 + */ +public interface ManagementContextResourceConfigCustomizer { + + /** + * Customize the resource config. + * @param config the {@link ResourceConfig} to customize + */ + void customize(ResourceConfig config); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/package-info.java new file mode 100644 index 0000000000..07432364bc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Configuration for a Jersey-based management context. + */ +package org.springframework.boot.actuate.autoconfigure.web.jersey; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports index 404fef621a..136ca59703 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports @@ -1,7 +1,10 @@ org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration +org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration org.springframework.boot.actuate.autoconfigure.security.servlet.SecurityRequestMatchersManagementContextConfiguration +org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration +org.springframework.boot.actuate.autoconfigure.web.jersey.JerseyChildManagementContextConfiguration org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementChildContextConfiguration org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementChildContextConfiguration org.springframework.boot.actuate.autoconfigure.web.servlet.WebMvcEndpointChildContextConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 751825bd6f..cf47b7bb35 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -70,6 +70,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.Wavefron org.springframework.boot.actuate.autoconfigure.metrics.graphql.GraphQlMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration +org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java index 0a0733b24f..c29492d83d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -18,17 +18,21 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web; import java.util.Collections; +import org.glassfish.jersey.server.ResourceConfig; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar; import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.DispatcherServlet; import static org.assertj.core.api.Assertions.assertThat; @@ -45,13 +49,24 @@ class ServletEndpointManagementContextConfigurationTests { @Test void contextShouldContainServletEndpointRegistrar() { - this.contextRunner.run((context) -> { + FilteredClassLoader classLoader = new FilteredClassLoader(ResourceConfig.class); + this.contextRunner.withClassLoader(classLoader).run((context) -> { assertThat(context).hasSingleBean(ServletEndpointRegistrar.class); ServletEndpointRegistrar bean = context.getBean(ServletEndpointRegistrar.class); assertThat(bean).hasFieldOrPropertyWithValue("basePath", "/test/actuator"); }); } + @Test + void contextWhenJerseyShouldContainServletEndpointRegistrar() { + FilteredClassLoader classLoader = new FilteredClassLoader(DispatcherServlet.class); + this.contextRunner.withClassLoader(classLoader).run((context) -> { + assertThat(context).hasSingleBean(ServletEndpointRegistrar.class); + ServletEndpointRegistrar bean = context.getBean(ServletEndpointRegistrar.class); + assertThat(bean).hasFieldOrPropertyWithValue("basePath", "/jersey/actuator"); + }); + } + @Test void contextWhenNoServletBasedShouldNotContainServletEndpointRegistrar() { new ApplicationContextRunner().withUserConfiguration(TestConfig.class) @@ -73,6 +88,11 @@ class ServletEndpointManagementContextConfigurationTests { return () -> "/test"; } + @Bean + JerseyApplicationPath jerseyApplicationPath() { + return () -> "/jersey"; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java new file mode 100644 index 0000000000..b826f9a9d9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey; + +import java.util.Set; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for web endpoints running on Jersey. + * + * @author Andy Wilkinson + */ +class JerseyWebEndpointIntegrationTests { + + @Test + void whenJerseyIsConfiguredToUseAFilterThenResourceRegistrationSucceeds() { + new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class, + JerseyAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + JerseyWebEndpointManagementContextConfiguration.class)) + .withUserConfiguration(ResourceConfigConfiguration.class) + .withClassLoader(new FilteredClassLoader(DispatcherServlet.class)) + .withPropertyValues("spring.jersey.type=filter", "server.port=0").run((context) -> { + assertThat(context).hasNotFailed(); + Set resources = context.getBean(ResourceConfig.class).getResources(); + assertThat(resources).hasSize(1); + Resource resource = resources.iterator().next(); + assertThat(resource.getPath()).isEqualTo("/actuator"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ResourceConfigConfiguration { + + @Bean + ResourceConfig resourceConfig() { + return new ResourceConfig(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java new file mode 100644 index 0000000000..d7b3bdd152 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey; + +import java.util.Collections; + +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration.JerseyWebEndpointsResourcesRegistrar; +import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration; +import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyWebEndpointManagementContextConfiguration}. + * + * @author Michael Simons + * @author Madhura Bhave + */ +class JerseyWebEndpointManagementContextConfigurationTests { + + private final WebApplicationContextRunner runner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebEndpointAutoConfiguration.class, + JerseyWebEndpointManagementContextConfiguration.class)) + .withBean(WebEndpointsSupplier.class, () -> Collections::emptyList); + + @Test + void jerseyWebEndpointsResourcesRegistrarForEndpointsIsAutoConfigured() { + this.runner.run((context) -> assertThat(context).hasSingleBean(JerseyWebEndpointsResourcesRegistrar.class)); + } + + @Test + void autoConfigurationIsConditionalOnServletWebApplication() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class)); + contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class)); + } + + @Test + void autoConfigurationIsConditionalOnClassResourceConfig() { + this.runner.withClassLoader(new FilteredClassLoader(ResourceConfig.class)) + .run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java new file mode 100644 index 0000000000..1e8de09dc6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.integrationtest; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; +import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * Integration tests for the Jersey actuator endpoints. + * + * @author Andy Wilkinson + * @author Madhura Bhave + */ +class JerseyEndpointIntegrationTests { + + @Test + void linksAreProvidedToAllEndpointTypes() { + testJerseyEndpoints(new Class[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class }); + } + + @Test + void linksPageIsNotAvailableWhenDisabled() { + getContextRunner(new Class[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class }) + .withPropertyValues("management.endpoints.web.discovery.enabled:false").run((context) -> { + int port = context + .getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build(); + client.get().uri("/actuator").exchange().expectStatus().isNotFound(); + }); + } + + @Test + void actuatorEndpointsWhenUserProvidedResourceConfigBeanNotAvailable() { + testJerseyEndpoints(new Class[] { EndpointsConfiguration.class }); + } + + @Test + void actuatorEndpointsWhenSecurityAvailable() { + WebApplicationContextRunner contextRunner = getContextRunner( + new Class[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class }, + getAutoconfigurations(SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class)); + contextRunner.run((context) -> { + int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build(); + client.get().uri("/actuator").exchange().expectStatus().isUnauthorized(); + }); + + } + + protected void testJerseyEndpoints(Class[] userConfigurations) { + getContextRunner(userConfigurations).run((context) -> { + int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build(); + client.get().uri("/actuator").exchange().expectStatus().isOk().expectBody().jsonPath("_links.beans") + .isNotEmpty().jsonPath("_links.restcontroller").doesNotExist().jsonPath("_links.controller") + .doesNotExist(); + }); + } + + WebApplicationContextRunner getContextRunner(Class[] userConfigurations, + Class... additionalAutoConfigurations) { + FilteredClassLoader classLoader = new FilteredClassLoader(DispatcherServlet.class); + return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withClassLoader(classLoader) + .withConfiguration(AutoConfigurations.of(getAutoconfigurations(additionalAutoConfigurations))) + .withUserConfiguration(userConfigurations) + .withPropertyValues("management.endpoints.web.exposure.include:*", "server.port:0"); + } + + private Class[] getAutoconfigurations(Class... additional) { + List> autoconfigurations = new ArrayList<>(Arrays.asList(JacksonAutoConfiguration.class, + JerseyAutoConfiguration.class, EndpointAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, WebEndpointAutoConfiguration.class, + ManagementContextAutoConfiguration.class, BeansEndpointAutoConfiguration.class)); + autoconfigurations.addAll(Arrays.asList(additional)); + return autoconfigurations.toArray(new Class[0]); + } + + @ControllerEndpoint(id = "controller") + static class TestControllerEndpoint { + + } + + @RestControllerEndpoint(id = "restcontroller") + static class TestRestControllerEndpoint { + + } + + @Configuration(proxyBeanMethods = false) + static class EndpointsConfiguration { + + @Bean + TestControllerEndpoint testControllerEndpoint() { + return new TestControllerEndpoint(); + } + + @Bean + TestRestControllerEndpoint testRestControllerEndpoint() { + return new TestRestControllerEndpoint(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ResourceConfigConfiguration { + + @Bean + ResourceConfig testResourceConfig() { + return new ResourceConfig(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyHealthEndpointAdditionalPathIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyHealthEndpointAdditionalPathIntegrationTests.java new file mode 100644 index 0000000000..95b2e1d4cc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyHealthEndpointAdditionalPathIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.integrationtest; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.web.context.ConfigurableWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * Integration tests for health groups on an additional path on Jersey. + * + * @author Madhura Bhave + */ +class JerseyHealthEndpointAdditionalPathIntegrationTests extends + AbstractHealthEndpointAdditionalPathIntegrationTests { + + JerseyHealthEndpointAdditionalPathIntegrationTests() { + super(new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, JerseyAutoConfiguration.class, + EndpointAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, + WebEndpointAutoConfiguration.class, JerseyAutoConfiguration.class, + ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, + HealthEndpointAutoConfiguration.class, DiskSpaceHealthContributorAutoConfiguration.class)) + .withInitializer(new ServerPortInfoApplicationContextInitializer()) + .withClassLoader(new FilteredClassLoader(DispatcherServlet.class)).withPropertyValues("server.port=0")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java new file mode 100644 index 0000000000..56ec7a79a1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.jersey; + +import java.net.URI; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider; +import io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider; +import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyServerMetricsAutoConfiguration}. + * + * @author Michael Weirauch + * @author Michael Simons + */ +class JerseyServerMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(JerseyServerMetricsAutoConfiguration.class)); + + private final WebApplicationContextRunner webContextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class, + JerseyServerMetricsAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, MetricsAutoConfiguration.class)) + .withUserConfiguration(ResourceConfiguration.class).withPropertyValues("server.port:0"); + + @Test + void shouldOnlyBeActiveInWebApplicationContext() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ResourceConfigCustomizer.class)); + } + + @Test + void shouldProvideAllNecessaryBeans() { + this.webContextRunner.run((context) -> assertThat(context).hasSingleBean(DefaultJerseyTagsProvider.class) + .hasSingleBean(ResourceConfigCustomizer.class)); + } + + @Test + void shouldHonorExistingTagProvider() { + this.webContextRunner.withUserConfiguration(CustomJerseyTagsProviderConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(CustomJerseyTagsProvider.class)); + } + + @Test + void httpRequestsAreTimed() { + this.webContextRunner.run((context) -> { + doRequest(context); + MeterRegistry registry = context.getBean(MeterRegistry.class); + Timer timer = registry.get("http.server.requests").tag("uri", "/users/{id}").timer(); + assertThat(timer.count()).isEqualTo(1); + }); + } + + @Test + void noHttpRequestsTimedWhenJerseyInstrumentationMissingFromClasspath() { + this.webContextRunner.withClassLoader(new FilteredClassLoader(MetricsApplicationEventListener.class)) + .run((context) -> { + doRequest(context); + + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("http.server.requests").timer()).isNull(); + }); + } + + private static void doRequest(AssertableWebApplicationContext context) { + int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getForEntity(URI.create("http://localhost:" + port + "/users/3"), String.class); + } + + @Configuration(proxyBeanMethods = false) + static class ResourceConfiguration { + + @Bean + ResourceConfig resourceConfig() { + return new ResourceConfig().register(new TestResource()); + } + + @Path("/users") + public class TestResource { + + @GET + @Path("/{id}") + public String getUser(@PathParam("id") String id) { + return id; + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomJerseyTagsProviderConfiguration { + + @Bean + JerseyTagsProvider customJerseyTagsProvider() { + return new CustomJerseyTagsProvider(); + } + + } + + static class CustomJerseyTagsProvider implements JerseyTagsProvider { + + @Override + public Iterable httpRequestTags(RequestEvent event) { + return null; + } + + @Override + public Iterable httpLongRequestTags(RequestEvent event) { + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java index afa72a9d1f..b51f302d40 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java @@ -43,6 +43,7 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.test.web.reactive.server.WebTestClient; @@ -76,10 +77,15 @@ abstract class AbstractEndpointRequestIntegrationTests { getContextRunner().run((context) -> { WebTestClient webTestClient = getWebTestClient(context); webTestClient.get().uri("/actuator").exchange().expectStatus().isOk(); - webTestClient.get().uri("/actuator/").exchange().expectStatus().isNotFound(); + webTestClient.get().uri("/actuator/").exchange().expectStatus() + .isEqualTo(expectedStatusWithTrailingSlash()); }); } + protected HttpStatus expectedStatusWithTrailingSlash() { + return HttpStatus.NOT_FOUND; + } + protected final WebApplicationContextRunner getContextRunner() { return createContextRunner().withPropertyValues("management.endpoints.web.exposure.include=*") .withUserConfiguration(BaseConfiguration.class, SecurityConfiguration.class).withConfiguration( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java new file mode 100644 index 0000000000..2fc10e076b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.security.servlet; + +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests for {@link EndpointRequest} with Jersey. + * + * @author Madhura Bhave + */ +class JerseyEndpointRequestIntegrationTests extends AbstractEndpointRequestIntegrationTests { + + @Test + void toLinksWhenApplicationPathSetShouldMatch() { + getContextRunner().withPropertyValues("spring.jersey.application-path=/admin").run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/admin/actuator/").exchange().expectStatus() + .isEqualTo(expectedStatusWithTrailingSlash()); + webTestClient.get().uri("/admin/actuator").exchange().expectStatus().isOk(); + }); + } + + @Test + void toEndpointWhenApplicationPathSetShouldMatch() { + getContextRunner().withPropertyValues("spring.jersey.application-path=/admin").run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/admin/actuator/e1").exchange().expectStatus().isOk(); + }); + } + + @Test + void toAnyEndpointWhenApplicationPathSetShouldMatch() { + getContextRunner() + .withPropertyValues("spring.jersey.application-path=/admin", "spring.security.user.password=password") + .run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/admin/actuator/e2").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/admin/actuator/e2").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + }); + } + + @Test + void toAnyEndpointShouldMatchServletEndpoint() { + getContextRunner().withPropertyValues("spring.security.user.password=password", + "management.endpoints.web.exposure.include=se1").run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/actuator/se1").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/actuator/se1").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + webTestClient.get().uri("/actuator/se1/list").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/actuator/se1/list").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + }); + } + + @Test + void toAnyEndpointWhenApplicationPathSetShouldMatchServletEndpoint() { + getContextRunner().withPropertyValues("spring.jersey.application-path=/admin", + "spring.security.user.password=password", "management.endpoints.web.exposure.include=se1") + .run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/admin/actuator/se1").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/admin/actuator/se1").header("Authorization", getBasicAuth()).exchange() + .expectStatus().isOk(); + webTestClient.get().uri("/admin/actuator/se1/list").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/admin/actuator/se1/list").header("Authorization", getBasicAuth()) + .exchange().expectStatus().isOk(); + }); + } + + @Override + protected HttpStatus expectedStatusWithTrailingSlash() { + return HttpStatus.OK; + } + + @Override + protected WebApplicationContextRunner createContextRunner() { + return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) + .withUserConfiguration(JerseyEndpointConfiguration.class) + .withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class)); + } + + @Configuration + @EnableConfigurationProperties(WebEndpointProperties.class) + static class JerseyEndpointConfiguration { + + @Bean + TomcatServletWebServerFactory tomcat() { + return new TomcatServletWebServerFactory(0); + } + + @Bean + ResourceConfig resourceConfig() { + return new ResourceConfig(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java index 23d67331b3..951252fb35 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider; import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -66,6 +67,17 @@ class SecurityRequestMatchersManagementContextConfigurationTests { }); } + @Test + void registersRequestMatcherForJerseyProviderIfJerseyPresentAndMvcAbsent() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) + .withUserConfiguration(TestJerseyConfiguration.class).run((context) -> { + AntPathRequestMatcherProvider matcherProvider = context + .getBean(AntPathRequestMatcherProvider.class); + RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example"); + assertThat(requestMatcher).extracting("pattern").isEqualTo("/admin/example"); + }); + } + @Test void mvcRequestMatcherProviderConditionalOnDispatcherServletClass() { this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) @@ -79,6 +91,20 @@ class SecurityRequestMatchersManagementContextConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); } + @Test + void jerseyRequestMatcherProviderConditionalOnResourceConfigClass() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.glassfish.jersey.server.ResourceConfig")) + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); + } + + @Test + void jerseyRequestMatcherProviderConditionalOnJerseyApplicationPathBean() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)) + .withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) + .run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class)); + } + @Configuration(proxyBeanMethods = false) static class TestMvcConfiguration { @@ -89,4 +115,14 @@ class SecurityRequestMatchersManagementContextConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class TestJerseyConfiguration { + + @Bean + JerseyApplicationPath jerseyApplicationPath() { + return () -> "/admin"; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java new file mode 100644 index 0000000000..cca272741d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.jersey; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JerseyChildManagementContextConfiguration}. + * + * @author Andy Wilkinson + * @author Madhura Bhave + */ +@ClassPathExclusions("spring-webmvc-*") +class JerseyChildManagementContextConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withUserConfiguration(JerseyChildManagementContextConfiguration.class); + + @Test + void autoConfigurationIsConditionalOnServletWebApplication() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class)); + contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class)); + } + + @Test + void autoConfigurationIsConditionalOnClassResourceConfig() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ResourceConfig.class)) + .run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class)); + } + + @Test + void jerseyApplicationPathIsAutoConfigured() { + this.contextRunner.run((context) -> { + JerseyApplicationPath bean = context.getBean(JerseyApplicationPath.class); + assertThat(bean.getPath()).isEqualTo("/"); + }); + } + + @Test + @SuppressWarnings("unchecked") + void servletRegistrationBeanIsAutoConfigured() { + this.contextRunner.run((context) -> { + ServletRegistrationBean bean = context.getBean(ServletRegistrationBean.class); + assertThat(bean.getUrlMappings()).containsExactly("/*"); + }); + } + + @Test + void resourceConfigCustomizerBeanIsNotRequired() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ResourceConfig.class)); + } + + @Test + void resourceConfigIsCustomizedWithResourceConfigCustomizerBean() { + this.contextRunner.withUserConfiguration(CustomizerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ResourceConfig.class); + ResourceConfig config = context.getBean(ResourceConfig.class); + ManagementContextResourceConfigCustomizer customizer = context + .getBean(ManagementContextResourceConfigCustomizer.class); + then(customizer).should().customize(config); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomizerConfiguration { + + @Bean + ManagementContextResourceConfigCustomizer resourceConfigCustomizer() { + return mock(ManagementContextResourceConfigCustomizer.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java new file mode 100644 index 0000000000..e8c8120d35 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.jersey; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JerseySameManagementContextConfiguration}. + * + * @author Madhura Bhave + */ +@ClassPathExclusions("spring-webmvc-*") +class JerseySameManagementContextConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class)); + + @Test + void autoConfigurationIsConditionalOnServletWebApplication() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class)); + contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class)); + } + + @Test + void autoConfigurationIsConditionalOnClassResourceConfig() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ResourceConfig.class)) + .run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class)); + } + + @Test + void jerseyApplicationPathIsAutoConfiguredWhenNeeded() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DefaultJerseyApplicationPath.class)); + } + + @Test + void jerseyApplicationPathIsConditionalOnMissingBean() { + this.contextRunner.withUserConfiguration(ConfigWithJerseyApplicationPath.class).run((context) -> { + assertThat(context).hasSingleBean(JerseyApplicationPath.class); + assertThat(context).hasBean("testJerseyApplicationPath"); + }); + } + + @Test + void existingResourceConfigBeanShouldNotAutoConfigureRelatedBeans() { + this.contextRunner.withUserConfiguration(ConfigWithResourceConfig.class).run((context) -> { + assertThat(context).hasSingleBean(ResourceConfig.class); + assertThat(context).doesNotHaveBean(JerseyApplicationPath.class); + assertThat(context).doesNotHaveBean(ServletRegistrationBean.class); + assertThat(context).hasBean("customResourceConfig"); + }); + } + + @Test + @SuppressWarnings("unchecked") + void servletRegistrationBeanIsAutoConfiguredWhenNeeded() { + this.contextRunner.withPropertyValues("spring.jersey.application-path=/jersey").run((context) -> { + ServletRegistrationBean bean = context.getBean(ServletRegistrationBean.class); + assertThat(bean.getUrlMappings()).containsExactly("/jersey/*"); + }); + } + + @Test + void resourceConfigIsCustomizedWithResourceConfigCustomizerBean() { + this.contextRunner.withUserConfiguration(CustomizerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ResourceConfig.class); + ResourceConfig config = context.getBean(ResourceConfig.class); + ManagementContextResourceConfigCustomizer customizer = context + .getBean(ManagementContextResourceConfigCustomizer.class); + then(customizer).should().customize(config); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConfigWithJerseyApplicationPath { + + @Bean + JerseyApplicationPath testJerseyApplicationPath() { + return mock(JerseyApplicationPath.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ConfigWithResourceConfig { + + @Bean + ResourceConfig customResourceConfig() { + return new ResourceConfig(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomizerConfiguration { + + @Bean + ManagementContextResourceConfigCustomizer resourceConfigCustomizer() { + return mock(ManagementContextResourceConfigCustomizer.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextConfigurationImportSelectorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextConfigurationImportSelectorTests.java index 17818f492e..6da00e7953 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextConfigurationImportSelectorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextConfigurationImportSelectorTests.java @@ -70,6 +70,10 @@ class ManagementContextConfigurationImportSelectorTests { .load(ManagementContextConfiguration.class, ManagementContextConfigurationImportSelectorTests.class.getClassLoader()) .forEach(expected::add); + // Remove JerseySameManagementContextConfiguration, as it specifies + // ManagementContextType.SAME and we asked for ManagementContextType.CHILD + expected.remove( + "org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration"); assertThat(imports).containsExactlyInAnyOrderElementsOf(expected); } diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index a675a6ebba..69529c1999 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -47,6 +47,8 @@ dependencies { exclude(group: "commons-logging", module: "commons-logging") } optional("org.flywaydb:flyway-core") + optional("org.glassfish.jersey.core:jersey-server") + optional("org.glassfish.jersey.containers:jersey-container-servlet-core") optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") optional("org.liquibase:liquibase-core") { @@ -88,6 +90,7 @@ dependencies { testImplementation("io.r2dbc:r2dbc-h2") testImplementation("org.apache.logging.log4j:log4j-to-slf4j") testImplementation("org.awaitility:awaitility") + testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson") testImplementation("org.hamcrest:hamcrest") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") @@ -101,5 +104,6 @@ dependencies { testRuntimeOnly("io.projectreactor.netty:reactor-netty-http") testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api") testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-el") + testRuntimeOnly("org.glassfish.jersey.ext:jersey-spring6") testRuntimeOnly("org.hsqldb:hsqldb") } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java new file mode 100644 index 0000000000..3fc88b0f6c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -0,0 +1,356 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web.jersey; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import org.glassfish.jersey.process.Inflector; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.server.model.Resource.Builder; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; +import org.springframework.boot.actuate.endpoint.InvocationContext; +import org.springframework.boot.actuate.endpoint.OperationArgumentResolver; +import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver; +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.Link; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * A factory for creating Jersey {@link Resource Resources} for {@link WebOperation web + * endpoint operations}. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 2.0.0 + */ +public class JerseyEndpointResourceFactory { + + /** + * Creates {@link Resource Resources} for the operations of the given + * {@code webEndpoints}. + * @param endpointMapping the base mapping for all endpoints + * @param endpoints the web endpoints + * @param endpointMediaTypes media types consumed and produced by the endpoints + * @param linksResolver resolver for determining links to available endpoints + * @param shouldRegisterLinks should register links + * @return the resources for the operations + */ + public Collection createEndpointResources(EndpointMapping endpointMapping, + Collection endpoints, EndpointMediaTypes endpointMediaTypes, + EndpointLinksResolver linksResolver, boolean shouldRegisterLinks) { + List resources = new ArrayList<>(); + endpoints.stream().flatMap((endpoint) -> endpoint.getOperations().stream()) + .map((operation) -> createResource(endpointMapping, operation)).forEach(resources::add); + if (shouldRegisterLinks) { + Resource resource = createEndpointLinksResource(endpointMapping.getPath(), endpointMediaTypes, + linksResolver); + resources.add(resource); + } + return resources; + } + + protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) { + WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate(); + String path = requestPredicate.getPath(); + String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable(); + if (matchAllRemainingPathSegmentsVariable != null) { + path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}", + "{" + matchAllRemainingPathSegmentsVariable + ": .*}"); + } + return getResource(endpointMapping, operation, requestPredicate, path, null, null); + } + + protected Resource getResource(EndpointMapping endpointMapping, WebOperation operation, + WebOperationRequestPredicate requestPredicate, String path, WebServerNamespace serverNamespace, + JerseyRemainingPathSegmentProvider remainingPathSegmentProvider) { + Builder resourceBuilder = Resource.builder().path(endpointMapping.getPath()) + .path(endpointMapping.createSubPath(path)); + resourceBuilder.addMethod(requestPredicate.getHttpMethod().name()) + .consumes(StringUtils.toStringArray(requestPredicate.getConsumes())) + .produces(StringUtils.toStringArray(requestPredicate.getProduces())) + .handledBy(new OperationInflector(operation, !requestPredicate.getConsumes().isEmpty(), serverNamespace, + remainingPathSegmentProvider)); + return resourceBuilder.build(); + } + + private Resource createEndpointLinksResource(String endpointPath, EndpointMediaTypes endpointMediaTypes, + EndpointLinksResolver linksResolver) { + Builder resourceBuilder = Resource.builder().path(endpointPath); + resourceBuilder.addMethod("GET").produces(StringUtils.toStringArray(endpointMediaTypes.getProduced())) + .handledBy(new EndpointLinksInflector(linksResolver)); + return resourceBuilder.build(); + } + + /** + * {@link Inflector} to invoke the {@link WebOperation}. + */ + private static final class OperationInflector implements Inflector { + + private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR; + + private static final List> BODY_CONVERTERS; + + static { + List> converters = new ArrayList<>(); + converters.add(new ResourceBodyConverter()); + if (ClassUtils.isPresent("reactor.core.publisher.Mono", OperationInflector.class.getClassLoader())) { + converters.add(new FluxBodyConverter()); + converters.add(new MonoBodyConverter()); + } + BODY_CONVERTERS = Collections.unmodifiableList(converters); + } + + private final WebOperation operation; + + private final boolean readBody; + + private final WebServerNamespace serverNamespace; + + private final JerseyRemainingPathSegmentProvider remainingPathSegmentProvider; + + private OperationInflector(WebOperation operation, boolean readBody, WebServerNamespace serverNamespace, + JerseyRemainingPathSegmentProvider remainingPathSegments) { + this.operation = operation; + this.readBody = readBody; + this.serverNamespace = serverNamespace; + this.remainingPathSegmentProvider = remainingPathSegments; + } + + @Override + public Response apply(ContainerRequestContext data) { + Map arguments = new HashMap<>(); + if (this.readBody) { + arguments.putAll(extractBodyArguments(data)); + } + arguments.putAll(extractPathParameters(data)); + arguments.putAll(extractQueryParameters(data)); + try { + JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext()); + OperationArgumentResolver serverNamespaceArgumentResolver = OperationArgumentResolver + .of(WebServerNamespace.class, () -> this.serverNamespace); + InvocationContext invocationContext = new InvocationContext(securityContext, arguments, + serverNamespaceArgumentResolver, + new ProducibleOperationArgumentResolver(() -> data.getHeaders().get("Accept"))); + Object response = this.operation.invoke(invocationContext); + return convertToJaxRsResponse(response, data.getRequest().getMethod()); + } + catch (InvalidEndpointRequestException ex) { + return Response.status(Status.BAD_REQUEST).build(); + } + } + + @SuppressWarnings("unchecked") + private Map extractBodyArguments(ContainerRequestContext data) { + Map entity = ((ContainerRequest) data).readEntity(Map.class); + return (entity != null) ? entity : Collections.emptyMap(); + } + + private Map extractPathParameters(ContainerRequestContext requestContext) { + Map pathParameters = extract(requestContext.getUriInfo().getPathParameters()); + String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate() + .getMatchAllRemainingPathSegmentsVariable(); + if (matchAllRemainingPathSegmentsVariable != null) { + String remainingPathSegments = getRemainingPathSegments(requestContext, pathParameters, + matchAllRemainingPathSegmentsVariable); + pathParameters.put(matchAllRemainingPathSegmentsVariable, tokenizePathSegments(remainingPathSegments)); + } + return pathParameters; + } + + private String getRemainingPathSegments(ContainerRequestContext requestContext, + Map pathParameters, String matchAllRemainingPathSegmentsVariable) { + if (this.remainingPathSegmentProvider != null) { + return this.remainingPathSegmentProvider.get(requestContext, matchAllRemainingPathSegmentsVariable); + } + return (String) pathParameters.get(matchAllRemainingPathSegmentsVariable); + } + + private String[] tokenizePathSegments(String path) { + String[] segments = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR, false, true); + for (int i = 0; i < segments.length; i++) { + if (segments[i].contains("%")) { + segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8); + } + } + return segments; + } + + private Map extractQueryParameters(ContainerRequestContext requestContext) { + return extract(requestContext.getUriInfo().getQueryParameters()); + } + + private Map extract(MultivaluedMap multivaluedMap) { + Map result = new HashMap<>(); + multivaluedMap.forEach((name, values) -> { + if (!CollectionUtils.isEmpty(values)) { + result.put(name, (values.size() != 1) ? values : values.get(0)); + } + }); + return result; + } + + private Response convertToJaxRsResponse(Object response, String httpMethod) { + if (response == null) { + boolean isGet = HttpMethod.GET.equals(httpMethod); + Status status = isGet ? Status.NOT_FOUND : Status.NO_CONTENT; + return Response.status(status).build(); + } + try { + if (!(response instanceof WebEndpointResponse)) { + return Response.status(Status.OK).entity(convertIfNecessary(response)).build(); + } + WebEndpointResponse webEndpointResponse = (WebEndpointResponse) response; + return Response.status(webEndpointResponse.getStatus()) + .header("Content-Type", webEndpointResponse.getContentType()) + .entity(convertIfNecessary(webEndpointResponse.getBody())).build(); + } + catch (IOException ex) { + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + } + + private Object convertIfNecessary(Object body) throws IOException { + for (Function converter : BODY_CONVERTERS) { + body = converter.apply(body); + } + return body; + } + + } + + /** + * Body converter from {@link org.springframework.core.io.Resource} to + * {@link InputStream}. + */ + private static final class ResourceBodyConverter implements Function { + + @Override + public Object apply(Object body) { + if (body instanceof org.springframework.core.io.Resource) { + try { + return ((org.springframework.core.io.Resource) body).getInputStream(); + } + catch (IOException ex) { + throw new IllegalStateException(); + } + } + return body; + } + + } + + /** + * Body converter from {@link Mono} to {@link Mono#block()}. + */ + private static final class MonoBodyConverter implements Function { + + @Override + public Object apply(Object body) { + if (body instanceof Mono) { + return ((Mono) body).block(); + } + return body; + } + + } + + /** + * Body converter from {@link Flux} to {@link Flux#collectList Mono<List>}. + */ + private static final class FluxBodyConverter implements Function { + + @Override + public Object apply(Object body) { + if (body instanceof Flux) { + return ((Flux) body).collectList(); + } + return body; + } + + } + + /** + * {@link Inflector} to for endpoint links. + */ + private static final class EndpointLinksInflector implements Inflector { + + private final EndpointLinksResolver linksResolver; + + private EndpointLinksInflector(EndpointLinksResolver linksResolver) { + this.linksResolver = linksResolver; + } + + @Override + public Response apply(ContainerRequestContext request) { + Map links = this.linksResolver + .resolveLinks(request.getUriInfo().getAbsolutePath().toString()); + return Response.ok(Collections.singletonMap("_links", links)).build(); + } + + } + + private static final class JerseySecurityContext implements SecurityContext { + + private final jakarta.ws.rs.core.SecurityContext securityContext; + + private JerseySecurityContext(jakarta.ws.rs.core.SecurityContext securityContext) { + this.securityContext = securityContext; + } + + @Override + public Principal getPrincipal() { + return this.securityContext.getUserPrincipal(); + } + + @Override + public boolean isUserInRole(String role) { + return this.securityContext.isUserInRole(role); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java new file mode 100644 index 0000000000..e5253de2da --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web.jersey; + +import java.util.Set; + +import org.glassfish.jersey.server.model.Resource; + +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; +import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath; +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroups; + +/** + * A factory for creating Jersey {@link Resource Resources} for health groups with + * additional path. + * + * @author Madhura Bhave + * @since 2.6.0 + */ +public class JerseyHealthEndpointAdditionalPathResourceFactory extends JerseyEndpointResourceFactory { + + private final Set groups; + + private final WebServerNamespace serverNamespace; + + public JerseyHealthEndpointAdditionalPathResourceFactory(WebServerNamespace serverNamespace, + HealthEndpointGroups groups) { + this.serverNamespace = serverNamespace; + this.groups = groups.getAllWithAdditionalPath(serverNamespace); + } + + @Override + protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) { + WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate(); + String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable(); + if (matchAllRemainingPathSegmentsVariable != null) { + for (HealthEndpointGroup group : this.groups) { + AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath(); + if (additionalPath != null) { + return getResource(endpointMapping, operation, requestPredicate, additionalPath.getValue(), + this.serverNamespace, (data, pathSegmentsVariable) -> data.getUriInfo().getPath()); + } + } + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyRemainingPathSegmentProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyRemainingPathSegmentProvider.java new file mode 100644 index 0000000000..b9366b3985 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyRemainingPathSegmentProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web.jersey; + +import jakarta.ws.rs.container.ContainerRequestContext; + +/** + * Strategy interface used to provide the remaining path segments for a Jersey actuator + * endpoint. + * + * @author Madhura Bhave + */ +interface JerseyRemainingPathSegmentProvider { + + String get(ContainerRequestContext requestContext, String matchAllRemainingPathSegmentsVariable); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/package-info.java new file mode 100644 index 0000000000..7fe3c1bac8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jersey support for actuator endpoints. + */ +package org.springframework.boot.actuate.endpoint.web.jersey; 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 6f1a5abb26..1648284866 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 @@ -123,7 +123,7 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("/test/").exchange().expectStatus().isNotFound()); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java new file mode 100644 index 0000000000..ee4669868e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web.jersey; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.ext.ContextResolver; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Integration tests for web endpoints exposed using Jersey. + * + * @author Andy Wilkinson + * @see JerseyEndpointResourceFactory + */ +public class JerseyWebEndpointIntegrationTests + extends AbstractWebEndpointIntegrationTests { + + public JerseyWebEndpointIntegrationTests() { + super(JerseyWebEndpointIntegrationTests::createApplicationContext, + JerseyWebEndpointIntegrationTests::applyAuthenticatedConfiguration); + } + + private static AnnotationConfigServletWebServerApplicationContext createApplicationContext() { + AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); + context.register(JerseyConfiguration.class); + return context; + } + + private static void applyAuthenticatedConfiguration(AnnotationConfigServletWebServerApplicationContext context) { + context.register(AuthenticatedConfiguration.class); + } + + @Override + protected int getPort(AnnotationConfigServletWebServerApplicationContext context) { + return context.getWebServer().getPort(); + } + + @Override + protected void validateErrorBody(WebTestClient.BodyContentSpec body, HttpStatus status, String path, + String message) { + // Jersey doesn't support the general error page handling + } + + @Override + @Test + @Disabled("Jersey does not distinguish between /example and /example/") + protected void operationWithTrailingSlashShouldNotMatch() { + } + + @Configuration(proxyBeanMethods = false) + static class JerseyConfiguration { + + @Bean + TomcatServletWebServerFactory tomcat() { + return new TomcatServletWebServerFactory(0); + } + + @Bean + ServletRegistrationBean servletContainer(ResourceConfig resourceConfig) { + return new ServletRegistrationBean<>(new ServletContainer(resourceConfig), "/*"); + } + + @Bean + ResourceConfig resourceConfig(Environment environment, WebEndpointDiscoverer endpointDiscoverer, + EndpointMediaTypes endpointMediaTypes) { + ResourceConfig resourceConfig = new ResourceConfig(); + String endpointPath = environment.getProperty("endpointPath"); + Collection resources = new JerseyEndpointResourceFactory().createEndpointResources( + new EndpointMapping(endpointPath), endpointDiscoverer.getEndpoints(), endpointMediaTypes, + new EndpointLinksResolver(endpointDiscoverer.getEndpoints()), StringUtils.hasText(endpointPath)); + resourceConfig.registerResources(new HashSet<>(resources)); + resourceConfig.register(JacksonFeature.class); + resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()), ContextResolver.class); + return resourceConfig; + } + + } + + @Configuration(proxyBeanMethods = false) + static class AuthenticatedConfiguration { + + @Bean + Filter securityFilter() { + return new OncePerRequestFilter() { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(new UsernamePasswordAuthenticationToken("Alice", "secret", + Arrays.asList(new SimpleGrantedAuthority("ROLE_ACTUATOR")))); + SecurityContextHolder.setContext(context); + try { + filterChain.doFilter(new SecurityContextHolderAwareRequestWrapper(request, "ROLE_"), response); + } + finally { + SecurityContextHolder.clearContext(); + } + } + + }; + } + + } + + private static final class ObjectMapperContextResolver implements ContextResolver { + + private final ObjectMapper objectMapper; + + private ObjectMapperContextResolver(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public ObjectMapper getContext(Class type) { + return this.objectMapper; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java index 4d2ca1883a..1bb08174fb 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java @@ -17,12 +17,16 @@ package org.springframework.boot.actuate.endpoint.web.test; import java.time.Duration; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.Extension; @@ -37,11 +41,14 @@ import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer; +import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -84,12 +91,22 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation public Stream provideTestTemplateInvocationContexts( ExtensionContext extensionContext) { return Stream.of( + new WebEndpointsInvocationContext("Jersey", + WebEndpointTestInvocationContextProvider::createJerseyContext), new WebEndpointsInvocationContext("WebMvc", WebEndpointTestInvocationContextProvider::createWebMvcContext), new WebEndpointsInvocationContext("WebFlux", WebEndpointTestInvocationContextProvider::createWebFluxContext)); } + private static ConfigurableApplicationContext createJerseyContext(List> classes) { + AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); + classes.add(JerseyEndpointConfiguration.class); + context.register(ClassUtils.toClassArray(classes)); + context.refresh(); + return context; + } + private static ConfigurableApplicationContext createWebMvcContext(List> classes) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); classes.add(WebMvcEndpointConfiguration.class); @@ -191,6 +208,44 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation } + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ JacksonAutoConfiguration.class, JerseyAutoConfiguration.class }) + static class JerseyEndpointConfiguration { + + private final ApplicationContext applicationContext; + + JerseyEndpointConfiguration(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Bean + TomcatServletWebServerFactory tomcat() { + return new TomcatServletWebServerFactory(0); + } + + @Bean + ResourceConfig resourceConfig() { + return new ResourceConfig(); + } + + @Bean + ResourceConfigCustomizer webEndpointRegistrar() { + return this::customize; + } + + private void customize(ResourceConfig config) { + EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT; + WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, + new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), + Collections.emptyList()); + Collection resources = new JerseyEndpointResourceFactory().createEndpointResources( + new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, + new EndpointLinksResolver(discoverer.getEndpoints()), true); + config.registerResources(new HashSet<>(resources)); + } + + } + @Configuration(proxyBeanMethods = false) @ImportAutoConfiguration({ JacksonAutoConfiguration.class, WebFluxAutoConfiguration.class }) static class WebFluxEndpointConfiguration implements ApplicationListener { diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 60db265f1e..851df7f452 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -19,6 +19,7 @@ dependencies { optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") optional("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + optional("com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations") optional("com.fasterxml.jackson.module:jackson-module-parameter-names") optional("com.google.code.gson:gson") optional("com.hazelcast:hazelcast") @@ -106,6 +107,11 @@ dependencies { optional("org.flywaydb:flyway-core") optional("org.flywaydb:flyway-sqlserver") optional("org.freemarker:freemarker") + optional("org.glassfish.jersey.containers:jersey-container-servlet-core") + optional("org.glassfish.jersey.containers:jersey-container-servlet") + optional("org.glassfish.jersey.core:jersey-server") + optional("org.glassfish.jersey.ext:jersey-spring6") + optional("org.glassfish.jersey.media:jersey-media-json-jackson") optional("org.hibernate.orm:hibernate-core") optional("org.hibernate.orm:hibernate-jcache") optional("org.hibernate.validator:hibernate-validator") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java new file mode 100644 index 0000000000..8f2aec3cb6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java @@ -0,0 +1,232 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.util.Collections; +import java.util.EnumSet; + +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.xml.bind.annotation.XmlElement; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spring.SpringComponentProvider; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.servlet.ServletProperties; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean; +import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.DynamicRegistrationBean; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.util.ClassUtils; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ServletContextAware; +import org.springframework.web.filter.RequestContextFilter; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Jersey. + * + * @author Dave Syer + * @author Andy Wilkinson + * @author Eddú Meléndez + * @author Stephane Nicoll + * @since 1.2.0 + */ +@AutoConfiguration(before = DispatcherServletAutoConfiguration.class, after = JacksonAutoConfiguration.class) +@ConditionalOnClass({ SpringComponentProvider.class, ServletRegistration.class }) +@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig") +@ConditionalOnWebApplication(type = Type.SERVLET) +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@EnableConfigurationProperties(JerseyProperties.class) +public class JerseyAutoConfiguration implements ServletContextAware { + + private static final Log logger = LogFactory.getLog(JerseyAutoConfiguration.class); + + private final JerseyProperties jersey; + + private final ResourceConfig config; + + public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config, + ObjectProvider customizers) { + this.jersey = jersey; + this.config = config; + customizers.orderedStream().forEach((customizer) -> customizer.customize(this.config)); + } + + @Bean + @ConditionalOnMissingFilterBean(RequestContextFilter.class) + public FilterRegistrationBean requestContextFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new RequestContextFilter()); + registration.setOrder(this.jersey.getFilter().getOrder() - 1); + registration.setName("requestContextFilter"); + return registration; + } + + @Bean + @ConditionalOnMissingBean + public JerseyApplicationPath jerseyApplicationPath() { + return new DefaultJerseyApplicationPath(this.jersey.getApplicationPath(), this.config); + } + + @Bean + @ConditionalOnMissingBean(name = "jerseyFilterRegistration") + @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "filter") + public FilterRegistrationBean jerseyFilterRegistration(JerseyApplicationPath applicationPath) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new ServletContainer(this.config)); + registration.setUrlPatterns(Collections.singletonList(applicationPath.getUrlMapping())); + registration.setOrder(this.jersey.getFilter().getOrder()); + registration.addInitParameter(ServletProperties.FILTER_CONTEXT_PATH, stripPattern(applicationPath.getPath())); + addInitParameters(registration); + registration.setName("jerseyFilter"); + registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class)); + return registration; + } + + private String stripPattern(String path) { + if (path.endsWith("/*")) { + path = path.substring(0, path.lastIndexOf("/*")); + } + return path; + } + + @Bean + @ConditionalOnMissingBean(name = "jerseyServletRegistration") + @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "servlet", matchIfMissing = true) + public ServletRegistrationBean jerseyServletRegistration(JerseyApplicationPath applicationPath) { + ServletRegistrationBean registration = new ServletRegistrationBean<>( + new ServletContainer(this.config), applicationPath.getUrlMapping()); + addInitParameters(registration); + registration.setName(getServletRegistrationName()); + registration.setLoadOnStartup(this.jersey.getServlet().getLoadOnStartup()); + return registration; + } + + private String getServletRegistrationName() { + return ClassUtils.getUserClass(this.config.getClass()).getName(); + } + + private void addInitParameters(DynamicRegistrationBean registration) { + this.jersey.getInit().forEach(registration::addInitParameter); + } + + @Override + public void setServletContext(ServletContext servletContext) { + String servletRegistrationName = getServletRegistrationName(); + ServletRegistration registration = servletContext.getServletRegistration(servletRegistrationName); + if (registration != null) { + if (logger.isInfoEnabled()) { + logger.info("Configuring existing registration for Jersey servlet '" + servletRegistrationName + "'"); + } + registration.setInitParameters(this.jersey.getInit()); + } + } + + @Order(Ordered.HIGHEST_PRECEDENCE) + public static final class JerseyWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + // We need to switch *off* the Jersey WebApplicationInitializer because it + // will try and register a ContextLoaderListener which we don't need + servletContext.setInitParameter("contextConfigLocation", ""); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(JacksonFeature.class) + @ConditionalOnSingleCandidate(ObjectMapper.class) + static class JacksonResourceConfigCustomizer { + + @Bean + ResourceConfigCustomizer resourceConfigCustomizer(final ObjectMapper objectMapper) { + return (ResourceConfig config) -> { + config.register(JacksonFeature.class); + config.register(new ObjectMapperContextResolver(objectMapper), ContextResolver.class); + }; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ JakartaXmlBindAnnotationIntrospector.class, XmlElement.class }) + static class JaxbObjectMapperCustomizer { + + @Autowired + void addJaxbAnnotationIntrospector(ObjectMapper objectMapper) { + JakartaXmlBindAnnotationIntrospector jaxbAnnotationIntrospector = new JakartaXmlBindAnnotationIntrospector( + objectMapper.getTypeFactory()); + objectMapper.setAnnotationIntrospectors( + createPair(objectMapper.getSerializationConfig(), jaxbAnnotationIntrospector), + createPair(objectMapper.getDeserializationConfig(), jaxbAnnotationIntrospector)); + } + + private AnnotationIntrospector createPair(MapperConfig config, + JakartaXmlBindAnnotationIntrospector jaxbAnnotationIntrospector) { + return AnnotationIntrospector.pair(config.getAnnotationIntrospector(), jaxbAnnotationIntrospector); + } + + } + + private static final class ObjectMapperContextResolver implements ContextResolver { + + private final ObjectMapper objectMapper; + + private ObjectMapperContextResolver(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public ObjectMapper getContext(Class type) { + return this.objectMapper; + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyProperties.java new file mode 100644 index 0000000000..c69cf2d530 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyProperties.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for Jersey. + * + * @author Dave Syer + * @author Eddú Meléndez + * @author Stephane Nicoll + * @since 1.2.0 + */ +@ConfigurationProperties(prefix = "spring.jersey") +public class JerseyProperties { + + /** + * Jersey integration type. + */ + private Type type = Type.SERVLET; + + /** + * Init parameters to pass to Jersey through the servlet or filter. + */ + private Map init = new HashMap<>(); + + private final Filter filter = new Filter(); + + private final Servlet servlet = new Servlet(); + + /** + * Path that serves as the base URI for the application. If specified, overrides the + * value of "@ApplicationPath". + */ + private String applicationPath; + + public Filter getFilter() { + return this.filter; + } + + public Servlet getServlet() { + return this.servlet; + } + + public Type getType() { + return this.type; + } + + public void setType(Type type) { + this.type = type; + } + + public Map getInit() { + return this.init; + } + + public void setInit(Map init) { + this.init = init; + } + + public String getApplicationPath() { + return this.applicationPath; + } + + public void setApplicationPath(String applicationPath) { + this.applicationPath = applicationPath; + } + + public enum Type { + + SERVLET, FILTER + + } + + public static class Filter { + + /** + * Jersey filter chain order. + */ + private int order; + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + } + + public static class Servlet { + + /** + * Load on startup priority of the Jersey servlet. + */ + private int loadOnStartup = -1; + + public int getLoadOnStartup() { + return this.loadOnStartup; + } + + public void setLoadOnStartup(int loadOnStartup) { + this.loadOnStartup = loadOnStartup; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/ResourceConfigCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/ResourceConfigCustomizer.java new file mode 100644 index 0000000000..0f50d03a9a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/ResourceConfigCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +/** + * Callback interface that can be implemented by beans wishing to customize Jersey's + * {@link ResourceConfig} before it is used. + * + * @author Eddú Meléndez + * @since 1.4.0 + */ +@FunctionalInterface +public interface ResourceConfigCustomizer { + + /** + * Customize the resource config. + * @param config the {@link ResourceConfig} to customize + */ + void customize(ResourceConfig config); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/package-info.java new file mode 100644 index 0000000000..7c3bf20bac --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Jersey. + */ +package org.springframework.boot.autoconfigure.jersey; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java new file mode 100644 index 0000000000..a718afe613 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.servlet; + +import jakarta.ws.rs.ApplicationPath; +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.boot.autoconfigure.jersey.JerseyProperties; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.util.StringUtils; + +/** + * Default implementation of {@link JerseyApplicationPath} that derives the path from + * {@link JerseyProperties} or the {@code @ApplicationPath} annotation. + * + * @author Madhura Bhave + * @since 2.1.0 + */ +public class DefaultJerseyApplicationPath implements JerseyApplicationPath { + + private final String applicationPath; + + private final ResourceConfig config; + + public DefaultJerseyApplicationPath(String applicationPath, ResourceConfig config) { + this.applicationPath = applicationPath; + this.config = config; + } + + @Override + public String getPath() { + return resolveApplicationPath(); + } + + private String resolveApplicationPath() { + if (StringUtils.hasLength(this.applicationPath)) { + return this.applicationPath; + } + // Jersey doesn't like to be the default servlet, so map to /* as a fallback + return MergedAnnotations.from(this.config.getApplication().getClass(), SearchStrategy.TYPE_HIERARCHY) + .get(ApplicationPath.class).getValue(MergedAnnotation.VALUE, String.class).orElse("/*"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java new file mode 100644 index 0000000000..4d31ab0329 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.servlet; + +import org.springframework.boot.web.servlet.ServletRegistrationBean; + +/** + * Interface that can be used by auto-configurations that need path details Jersey's + * application path that serves as the base URI for the application. + * + * @author Madhura Bhave + * @since 2.0.7 + */ +@FunctionalInterface +public interface JerseyApplicationPath { + + /** + * Returns the configured path of the application. + * @return the configured path + */ + String getPath(); + + /** + * Return a form of the given path that's relative to the Jersey application path. + * @param path the path to make relative + * @return the relative path + */ + default String getRelativePath(String path) { + String prefix = getPrefix(); + if (!path.startsWith("/")) { + path = "/" + path; + } + return prefix + path; + } + + /** + * Return a cleaned up version of the path that can be used as a prefix for URLs. The + * resulting path will have path will not have a trailing slash. + * @return the prefix + * @see #getRelativePath(String) + */ + default String getPrefix() { + String result = getPath(); + int index = result.indexOf('*'); + if (index != -1) { + result = result.substring(0, index); + } + if (result.endsWith("/")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + /** + * Return a URL mapping pattern that can be used with a + * {@link ServletRegistrationBean} to map Jersey's servlet. + * @return the path as a servlet URL mapping + */ + default String getUrlMapping() { + String path = getPath(); + if (!path.startsWith("/")) { + path = "/" + path; + } + if (path.equals("/")) { + return "/*"; + } + if (path.contains("*")) { + return path; + } + if (path.endsWith("/")) { + return path + "*"; + } + return path + "/*"; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index aa1780d4a8..6d9791a5c7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1550,6 +1550,10 @@ "level": "error" } }, + { + "name": "spring.jersey.type", + "defaultValue": "servlet" + }, { "name": "spring.jpa.hibernate.use-new-id-generator-mappings", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 4323300b78..b9719cf4f4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -72,6 +72,7 @@ org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration +org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomApplicationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomApplicationTests.java new file mode 100644 index 0000000000..387c70281f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomApplicationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using a custom {@link Application}. + * + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +class JerseyAutoConfigurationCustomApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/test/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @ApplicationPath("/test") + static class TestApplication extends Application { + + } + + @Path("/hello") + public static class TestController { + + @GET + public String message() { + return "Hello World"; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + static class TestConfiguration { + + @Configuration(proxyBeanMethods = false) + public class JerseyConfiguration { + + @Bean + public TestApplication testApplication() { + return new TestApplication(); + } + + @Bean + public ResourceConfig conf(TestApplication app) { + ResourceConfig config = ResourceConfig.forApplication(app); + config.register(TestController.class); + return config; + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java new file mode 100644 index 0000000000..869beead28 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom servlet paths. + * + * @author Dave Syer + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "spring.jersey.type=filter", "server.servlet.context-path=/app", + "server.servlet.register-default-servlet=true" }) +@DirtiesContext +class JerseyAutoConfigurationCustomFilterContextPathTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/rest/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @MinimalWebConfiguration + @ApplicationPath("/rest") + @Path("/hello") + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java new file mode 100644 index 0000000000..cd40136bf3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom servlet paths. + * + * @author Dave Syer + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) +@DirtiesContext +class JerseyAutoConfigurationCustomFilterPathTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/rest/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @MinimalWebConfiguration + @ApplicationPath("rest") + @Path("/hello") + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomLoadOnStartupTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomLoadOnStartupTests.java new file mode 100644 index 0000000000..b35725de0f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomLoadOnStartupTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom load on startup. + * + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jersey.servlet.load-on-startup=5") +@DirtiesContext +class JerseyAutoConfigurationCustomLoadOnStartupTests { + + @Autowired + private ApplicationContext context; + + @Test + void contextLoads() { + assertThat(this.context.getBean("jerseyServletRegistration")).hasFieldOrPropertyWithValue("loadOnStartup", 5); + } + + @MinimalWebConfiguration + static class Application extends ResourceConfig { + + Application() { + register(Application.class); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java new file mode 100644 index 0000000000..0f0ba719b5 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom ObjectMapper. + * + * @author Eddú Meléndez + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "spring.jackson.default-property-inclusion=non_null") +@DirtiesContext +class JerseyAutoConfigurationCustomObjectMapperProviderTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity response = this.restTemplate.getForEntity("/rest/message", String.class); + assertThat(HttpStatus.OK).isEqualTo(response.getStatusCode()); + assertThat("{\"subject\":\"Jersey\"}").isEqualTo(response.getBody()); + } + + @MinimalWebConfiguration + @ApplicationPath("/rest") + @Path("/message") + public static class Application extends ResourceConfig { + + Application() { + register(Application.class); + } + + @GET + public Message message() { + return new Message("Jersey", null); + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + public static class Message { + + private String subject; + + private String body; + + Message(String subject, String body) { + this.subject = subject; + this.body = body; + } + + public String getSubject() { + return this.subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getBody() { + return this.body; + } + + public void setBody(String body) { + this.body = body; + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JacksonAutoConfiguration.class, + JerseyAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletContextPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletContextPathTests.java new file mode 100644 index 0000000000..65a806fd51 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletContextPathTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom servlet paths. + * + * @author Dave Syer + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "server.servlet.contextPath=/app") +@DirtiesContext +class JerseyAutoConfigurationCustomServletContextPathTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/rest/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @MinimalWebConfiguration + @ApplicationPath("/rest") + @Path("/hello") + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletPathTests.java new file mode 100644 index 0000000000..46cd436bb7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomServletPathTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom servlet paths. + * + * @author Dave Syer + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +class JerseyAutoConfigurationCustomServletPathTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/rest/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @MinimalWebConfiguration + @ApplicationPath("/rest") + @Path("/hello") + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java new file mode 100644 index 0000000000..085cb5df26 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom servlet paths. + * + * @author Dave Syer + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) +@DirtiesContext +class JerseyAutoConfigurationDefaultFilterPathTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @MinimalWebConfiguration + @Path("/hello") + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultServletPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultServletPathTests.java new file mode 100644 index 0000000000..359f67433c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultServletPathTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using default servlet paths. + * + * @author Dave Syer + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +class JerseyAutoConfigurationDefaultServletPathTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @MinimalWebConfiguration + @Path("/hello") + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java new file mode 100644 index 0000000000..e133626346 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.xml.bind.annotation.XmlTransient; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} with an ObjectMapper. + * + * @author Eddú Meléndez + * @author Andy Wilkinson + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "spring.jackson.default-property-inclusion:non-null") +@DirtiesContext +class JerseyAutoConfigurationObjectMapperProviderTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void responseIsSerializedUsingAutoConfiguredObjectMapper() { + ResponseEntity response = this.restTemplate.getForEntity("/rest/message", String.class); + assertThat(HttpStatus.OK).isEqualTo(response.getStatusCode()); + assertThat(response.getBody()).isEqualTo("{\"subject\":\"Jersey\"}"); + } + + @MinimalWebConfiguration + @ApplicationPath("/rest") + @Path("/message") + public static class Application extends ResourceConfig { + + Application() { + register(Application.class); + } + + @GET + public Message message() { + return new Message("Jersey", null); + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + public static class Message { + + private String subject; + + private String body; + + Message() { + } + + Message(String subject, String body) { + this.subject = subject; + this.body = body; + } + + public String getSubject() { + return this.subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getBody() { + return this.body; + } + + public void setBody(String body) { + this.body = body; + } + + @XmlTransient + public String getFoo() { + return "foo"; + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JacksonAutoConfiguration.class, + JerseyAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationServletContainerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationServletContainerTests.java new file mode 100644 index 0000000000..c840532382 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationServletContainerTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.nio.charset.StandardCharsets; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.tomcat.util.buf.UDecoder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfigurationServletContainerTests.Application; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that verify the behavior when deployed to a Servlet container where Jersey may + * have already initialized itself. + * + * @author Andy Wilkinson + */ +@SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +@ExtendWith(OutputCaptureExtension.class) +class JerseyAutoConfigurationServletContainerTests { + + @Test + void existingJerseyServletIsAmended(CapturedOutput output) { + assertThat(output).contains("Configuring existing registration for Jersey servlet"); + assertThat(output).contains("Servlet " + Application.class.getName() + " was not registered"); + } + + @ImportAutoConfiguration({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + @Import(ContainerConfiguration.class) + @Path("/hello") + @Configuration(proxyBeanMethods = false) + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + } + + @Configuration(proxyBeanMethods = false) + static class ContainerConfiguration { + + @Bean + TomcatServletWebServerFactory tomcat() { + return new TomcatServletWebServerFactory() { + + @Override + protected void postProcessContext(Context context) { + Wrapper jerseyServlet = context.createWrapper(); + String servletName = Application.class.getName(); + jerseyServlet.setName(servletName); + jerseyServlet.setServletClass(ServletContainer.class.getName()); + jerseyServlet.setServlet(new ServletContainer()); + jerseyServlet.setOverridable(false); + context.addChild(jerseyServlet); + String pattern = UDecoder.URLDecode("/*", StandardCharsets.UTF_8); + context.addServletMappingDecoded(pattern, servletName); + } + + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java new file mode 100644 index 0000000000..a6de7ce60a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.RequestContextFilter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration}. + * + * @author Andy Wilkinson + */ +class JerseyAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class)) + .withUserConfiguration(ResourceConfigConfiguration.class); + + @Test + void requestContextFilterRegistrationIsAutoConfigured() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); + assertThat(registration.getFilter()).isInstanceOf(RequestContextFilter.class); + }); + } + + @Test + void whenUserDefinesARequestContextFilterTheAutoConfiguredRegistrationBacksOff() { + this.contextRunner.withUserConfiguration(RequestContextFilterConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(FilterRegistrationBean.class); + assertThat(context).hasSingleBean(RequestContextFilter.class); + }); + } + + @Test + void whenUserDefinesARequestContextFilterRegistrationTheAutoConfiguredRegistrationBacksOff() { + this.contextRunner.withUserConfiguration(RequestContextFilterRegistrationConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + assertThat(context).hasBean("customRequestContextFilterRegistration"); + }); + } + + @Test + void whenJaxbIsAvailableTheObjectMapperIsCustomizedWithAnAnnotationIntrospector() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + assertThat(objectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors().stream() + .filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).hasSize(1); + }); + } + + @Test + void whenJaxbIsNotAvailableTheObjectMapperCustomizationBacksOff() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader("jakarta.xml.bind.annotation")).run((context) -> { + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + assertThat(objectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors() + .stream().filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).isEmpty(); + }); + } + + @Test + void whenJacksonJaxbModuleIsNotAvailableTheObjectMapperCustomizationBacksOff() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(JakartaXmlBindAnnotationIntrospector.class)).run((context) -> { + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + assertThat(objectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors() + .stream().filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).isEmpty(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ResourceConfigConfiguration { + + @Bean + ResourceConfig resourceConfig() { + return new ResourceConfig(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class RequestContextFilterConfiguration { + + @Bean + RequestContextFilter requestContextFilter() { + return new RequestContextFilter(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class RequestContextFilterRegistrationConfiguration { + + @Bean + FilterRegistrationBean customRequestContextFilterRegistration() { + return new FilterRegistrationBean<>(new RequestContextFilter()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationWithoutApplicationPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationWithoutApplicationPathTests.java new file mode 100644 index 0000000000..f67bee8c77 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationWithoutApplicationPathTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyAutoConfiguration} when using custom application path. + * + * @author Eddú Meléndez + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jersey.application-path=/api") +@DirtiesContext +class JerseyAutoConfigurationWithoutApplicationPathTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/api/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @MinimalWebConfiguration + @Path("/hello") + public static class Application extends ResourceConfig { + + @Value("${message:World}") + private String msg; + + Application() { + register(Application.class); + } + + @GET + public String message() { + return "Hello " + this.msg; + } + + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPathTests.java new file mode 100644 index 0000000000..c6fa208580 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPathTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.servlet; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyApplicationPath}. + * + * @author Madhura Bhave + */ +class JerseyApplicationPathTests { + + @Test + void getRelativePathReturnsRelativePath() { + assertThat(((JerseyApplicationPath) () -> "spring").getRelativePath("boot")).isEqualTo("spring/boot"); + assertThat(((JerseyApplicationPath) () -> "spring/").getRelativePath("boot")).isEqualTo("spring/boot"); + assertThat(((JerseyApplicationPath) () -> "spring").getRelativePath("/boot")).isEqualTo("spring/boot"); + assertThat(((JerseyApplicationPath) () -> "spring/*").getRelativePath("/boot")).isEqualTo("spring/boot"); + } + + @Test + void getPrefixWhenHasSimplePathReturnPath() { + assertThat(((JerseyApplicationPath) () -> "spring").getPrefix()).isEqualTo("spring"); + } + + @Test + void getPrefixWhenHasPatternRemovesPattern() { + assertThat(((JerseyApplicationPath) () -> "spring/*.do").getPrefix()).isEqualTo("spring"); + } + + @Test + void getPrefixWhenPathEndsWithSlashRemovesSlash() { + assertThat(((JerseyApplicationPath) () -> "spring/").getPrefix()).isEqualTo("spring"); + } + + @Test + void getUrlMappingWhenPathIsEmptyReturnsSlash() { + assertThat(((JerseyApplicationPath) () -> "").getUrlMapping()).isEqualTo("/*"); + } + + @Test + void getUrlMappingWhenPathIsSlashReturnsSlash() { + assertThat(((JerseyApplicationPath) () -> "/").getUrlMapping()).isEqualTo("/*"); + } + + @Test + void getUrlMappingWhenPathContainsStarReturnsPath() { + assertThat(((JerseyApplicationPath) () -> "/spring/*.do").getUrlMapping()).isEqualTo("/spring/*.do"); + } + + @Test + void getUrlMappingWhenHasPathNotEndingSlashReturnsSlashStarPattern() { + assertThat(((JerseyApplicationPath) () -> "/spring/boot").getUrlMapping()).isEqualTo("/spring/boot/*"); + } + + @Test + void getUrlMappingWhenHasPathDoesNotStartWithSlashPrependsSlash() { + assertThat(((JerseyApplicationPath) () -> "spring/boot").getUrlMapping()).isEqualTo("/spring/boot/*"); + } + + @Test + void getUrlMappingWhenHasPathEndingWithSlashReturnsSlashStarPattern() { + assertThat(((JerseyApplicationPath) () -> "/spring/boot/").getUrlMapping()).isEqualTo("/spring/boot/*"); + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0cae206daa..3c6cc0a8ee 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -692,6 +692,13 @@ bom { ] } } + library("Jersey", "3.0.6") { + group("org.glassfish.jersey") { + imports = [ + "jersey-bom" + ] + } + } library("Jetty EL", "10.0.14") { group("org.mortbay.jasper") { modules = [ @@ -1293,6 +1300,7 @@ bom { "spring-boot-starter-hateoas", "spring-boot-starter-integration", "spring-boot-starter-jdbc", + "spring-boot-starter-jersey", "spring-boot-starter-jetty", "spring-boot-starter-jooq", "spring-boot-starter-json", diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 15cba5524d..6401b47210 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -96,6 +96,8 @@ dependencies { implementation("org.assertj:assertj-core") implementation("org.cache2k:cache2k-spring") implementation("org.apache.groovy:groovy") + implementation("org.glassfish.jersey.containers:jersey-container-servlet-core") + implementation("org.glassfish.jersey.core:jersey-server") implementation("org.hibernate.orm:hibernate-jcache") { exclude group: "javax.activation", module: "javax.activation-api" exclude group: "javax.persistence", module: "javax.persistence-api" diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc index 77be093356..8797e6be27 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc @@ -90,7 +90,7 @@ The following technology-agnostic endpoints are available: | Performs a thread dump. |=== -If your application is a web application (Spring MVC or Spring WebFlux), you can use the following additional endpoints: +If your application is a web application (Spring MVC, Spring WebFlux, or Jersey), you can use the following additional endpoints: [cols="2,5"] |=== @@ -424,7 +424,8 @@ TIP: See {spring-boot-actuator-autoconfigure-module-code}/endpoint/web/CorsEndpo [[actuator.endpoints.implementing-custom]] === Implementing Custom Endpoints If you add a `@Bean` annotated with `@Endpoint`, any methods annotated with `@ReadOperation`, `@WriteOperation`, or `@DeleteOperation` are automatically exposed over JMX and, in a web application, over HTTP as well. -Endpoints can be exposed over HTTP by using Spring MVC, or Spring WebFlux. +Endpoints can be exposed over HTTP by using Jersey, Spring MVC, or Spring WebFlux. +If both Jersey and Spring MVC are available, Spring MVC is used. The following example exposes a read operation that returns a custom object: @@ -481,7 +482,8 @@ Before calling an operation method, the input received over JMX or HTTP is conve [[actuator.endpoints.implementing-custom.web]] ==== Custom Web Endpoints -Operations on an `@Endpoint`, `@WebEndpoint`, or `@EndpointWebExtension` are automatically exposed over HTTP using Spring MVC or Spring WebFlux. +Operations on an `@Endpoint`, `@WebEndpoint`, or `@EndpointWebExtension` are automatically exposed over HTTP using Jersey, Spring MVC, or Spring WebFlux. +If both Jersey and Spring MVC are available, Spring MVC is used. @@ -560,7 +562,9 @@ If an operation is invoked without a required parameter or with a parameter that [[actuator.endpoints.implementing-custom.web.range-requests]] ===== Web Endpoint Range Requests You can use an HTTP range request to request part of an HTTP resource. -Operations that return a `org.springframework.core.io.Resource` automatically support range requests. +When using Spring MVC or Spring Web Flux, operations that return a `org.springframework.core.io.Resource` automatically support range requests. + +NOTE: Range requests are not supported when using Jersey. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index b0bdbe2130..c1c4b9f735 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -819,6 +819,41 @@ Applications can opt in and record exceptions by <> for details). +If you do not want to record metrics for all Jersey requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. + +By default, Jersey server metrics are tagged with the following information: + +|=== +| Tag | Description + +| `exception` +| The simple class name of any exception that was thrown while handling the request. + +| `method` +| The request's method (for example, `GET` or `POST`) + +| `outcome` +| The request's outcome, based on the status code of the response. + 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx is `CLIENT_ERROR`, and 5xx is `SERVER_ERROR` + +| `status` +| The response's HTTP status code (for example, `200` or `500`) + +| `uri` +| The request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) +|=== + +To customize the tags, provide a `@Bean` that implements `JerseyTagsProvider`. + + + [[actuator.metrics.supported.http-clients]] ==== HTTP Client Metrics Spring Boot Actuator manages the instrumentation of both `RestTemplate` and `WebClient`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/monitoring.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/monitoring.adoc index c6daca0937..877f39541d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/monitoring.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/monitoring.adoc @@ -4,7 +4,8 @@ If you are developing a web application, Spring Boot Actuator auto-configures al The default convention is to use the `id` of the endpoint with a prefix of `/actuator` as the URL path. For example, `health` is exposed as `/actuator/health`. -TIP: Actuator is supported natively with Spring MVC and Spring WebFlux. +TIP: Actuator is supported natively with Spring MVC, Spring WebFlux, and Jersey. +If both Jersey and Spring MVC are available, Spring MVC is used. NOTE: Jackson is a required dependency in order to get the correct JSON responses as documented in the API documentation ({spring-boot-actuator-restapi-docs}[HTML] or {spring-boot-actuator-restapi-pdfdocs}[PDF]). diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties index 7f3437caef..1bc8f29508 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -186,6 +186,7 @@ boot-features-webflux-template-engines=features.developing-web-applications.spri boot-features-webflux-error-handling=features.developing-web-applications.spring-webflux.error-handling boot-features-webflux-error-handling-custom-error-pages=features.developing-web-applications.spring-webflux.error-handling.error-pages boot-features-webflux-web-filters=features.developing-web-applications.spring-webflux.web-filters +boot-features-jersey=features.developing-web-applications.jersey boot-features-embedded-container=features.developing-web-applications.embedded-container boot-features-embedded-container-servlets-filters-listeners=features.developing-web-applications.embedded-container.servlets-filters-listeners boot-features-embedded-container-servlets-filters-listeners-beans=features.developing-web-applications.embedded-container.servlets-filters-listeners.beans @@ -472,6 +473,7 @@ production-ready-metrics-system=actuator.metrics.supported.system production-ready-metrics-logger=actuator.metrics.supported.logger production-ready-metrics-spring-mvc=actuator.metrics.supported.spring-mvc production-ready-metrics-web-flux=actuator.metrics.supported.spring-webflux +production-ready-metrics-jersey-server=actuator.metrics.supported.jersey production-ready-metrics-http-clients=actuator.metrics.supported.http-clients production-ready-metrics-tomcat=actuator.metrics.supported.tomcat production-ready-metrics-cache=actuator.metrics.supported.cache @@ -615,6 +617,9 @@ howto-switch-off-the-spring-mvc-dispatcherservlet=howto.spring-mvc.switch-off-di howto-switch-off-default-mvc-configuration=howto.spring-mvc.switch-off-default-configuration howto-customize-view-resolvers=howto.spring-mvc.customize-view-resolvers howto-use-test-with-spring-security=howto.spring-mvc.testing.with-spring-security +howto-jersey=howto.jersey +howto-jersey-spring-security=howto.jersey.spring-security +howto-jersey-alongside-another-web-framework=howto.jersey.alongside-another-web-framework howto-http-clients=howto.http-clients howto-http-clients-proxy-configuration=howto.http-clients.rest-template-proxy-configuration howto-webclient-reactor-netty-customization=howto.http-clients.webclient-reactor-netty-customization @@ -768,6 +773,7 @@ features.developing-web-applications.spring-mvc.error-handling.error-pages=web.s features.developing-web-applications.spring-mvc.error-handling.error-pages-without-spring-mvc=web.servlet.spring-mvc.error-handling.error-pages-without-spring-mvc features.developing-web-applications.spring-mvc.error-handling.in-a-war-deployment=web.servlet.spring-mvc.error-handling.in-a-war-deployment features.developing-web-applications.spring-mvc.cors=web.servlet.spring-mvc.cors +features.developing-web-applications.jersey=web.servlet.jersey features.developing-web-applications.embedded-container=web.servlet.embedded-container features.developing-web-applications.embedded-container.servlets-filters-listeners=web.servlet.embedded-container.servlets-filters-listeners features.developing-web-applications.embedded-container.servlets-filters-listeners.beans=web.servlet.embedded-container.servlets-filters-listeners.beans diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/web.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/web.adoc index 8b2599df20..a2705125fa 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/web.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/web.adoc @@ -2,7 +2,7 @@ == Web If you develop Spring Boot web applications, take a look at the following content: -* *Servlet Web Applications:* <> +* *Servlet Web Applications:* <> * *Reactive Web Applications:* <> * *Graceful Shutdown:* <> * *Spring Security:* <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc index 55148fe5e4..ed1b2b90ce 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc @@ -23,6 +23,8 @@ include::howto/webserver.adoc[] include::howto/spring-mvc.adoc[] +include::howto/jersey.adoc[] + include::howto/http-clients.adoc[] include::howto/logging.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/jersey.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/jersey.adoc new file mode 100644 index 0000000000..ff1246e543 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/jersey.adoc @@ -0,0 +1,30 @@ +[[howto.jersey]] +== Jersey + + + +[[howto.jersey.spring-security]] +=== Secure Jersey endpoints with Spring Security +Spring Security can be used to secure a Jersey-based web application in much the same way as it can be used to secure a Spring MVC-based web application. +However, if you want to use Spring Security's method-level security with Jersey, you must configure Jersey to use `setStatus(int)` rather `sendError(int)`. +This prevents Jersey from committing the response before Spring Security has had an opportunity to report an authentication or authorization failure to the client. + +The `jersey.config.server.response.setStatusOverSendError` property must be set to `true` on the application's `ResourceConfig` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java[] +---- + + + +[[howto.jersey.alongside-another-web-framework]] +=== Use Jersey Alongside Another Web Framework +To use Jersey alongside another web framework, such as Spring MVC, it should be configured so that it will allow the other framework to handle requests that it cannot handle. +First, configure Jersey to use a filter rather than a servlet by configuring the configprop:spring.jersey.type[] application property with a value of `filter`. +Second, configure your `ResourceConfig` to forward requests that would have resulted in a 404, as shown in the following example. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/jersey/alongsideanotherwebframework/JerseyConfig.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc index d964554d77..58136d5297 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc @@ -1,6 +1,6 @@ [[web.servlet]] == Servlet Web Applications -If you want to build servlet-based web applications, you can take advantage of Spring Boot's auto-configuration for Spring MVC. +If you want to build servlet-based web applications, you can take advantage of Spring Boot's auto-configuration for Spring MVC or Jersey. @@ -422,6 +422,48 @@ include::code:MyCorsConfiguration[] +[[web.servlet.jersey]] +=== JAX-RS and Jersey +If you prefer the JAX-RS programming model for REST endpoints, you can use one of the available implementations instead of Spring MVC. +https://jersey.github.io/[Jersey] and https://cxf.apache.org/[Apache CXF] work quite well out of the box. +CXF requires you to register its `Servlet` or `Filter` as a `@Bean` in your application context. +Jersey has some native Spring support, so we also provide auto-configuration support for it in Spring Boot, together with a starter. + +To get started with Jersey, include the `spring-boot-starter-jersey` as a dependency and then you need one `@Bean` of type `ResourceConfig` in which you register all the endpoints, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/web/servlet/jersey/MyJerseyConfig.java[] +---- + +WARNING: Jersey's support for scanning executable archives is rather limited. +For example, it cannot scan for endpoints in a package found in a <> or in `WEB-INF/classes` when running an executable war file. +To avoid this limitation, the `packages` method should not be used, and endpoints should be registered individually by using the `register` method, as shown in the preceding example. + +For more advanced customizations, you can also register an arbitrary number of beans that implement `ResourceConfigCustomizer`. + +All the registered endpoints should be `@Components` with HTTP resource annotations (`@GET` and others), as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/web/servlet/jersey/MyEndpoint.java[] +---- + +Since the `Endpoint` is a Spring `@Component`, its lifecycle is managed by Spring and you can use the `@Autowired` annotation to inject dependencies and use the `@Value` annotation to inject external configuration. +By default, the Jersey servlet is registered and mapped to `/*`. +You can change the mapping by adding `@ApplicationPath` to your `ResourceConfig`. + +By default, Jersey is set up as a servlet in a `@Bean` of type `ServletRegistrationBean` named `jerseyServletRegistration`. +By default, the servlet is initialized lazily, but you can customize that behavior by setting `spring.jersey.servlet.load-on-startup`. +You can disable or override that bean by creating one of your own with the same name. +You can also use a filter instead of a servlet by setting `spring.jersey.type=filter` (in which case, the `@Bean` to replace or override is `jerseyFilterRegistration`). +The filter has an `@Order`, which you can set with `spring.jersey.filter.order`. +When using Jersey as a filter, a servlet that will handle any requests that are not intercepted by Jersey must be present. +If your application does not contain such a servlet, you may want to enable the default servlet by setting configprop:server.servlet.register-default-servlet[] to `true`. +Both the servlet and the filter registrations can be given init parameters by using `spring.jersey.init.*` to specify a map of properties. + + + [[web.servlet.embedded-container]] === Embedded Servlet Container Support For servlet application, Spring Boot includes support for embedded https://tomcat.apache.org/[Tomcat], https://www.eclipse.org/jetty/[Jetty], and https://github.com/undertow-io/undertow[Undertow] servers. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java new file mode 100644 index 0000000000..270267ce8a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.alongsideanotherwebframework; + +class Endpoint { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java new file mode 100644 index 0000000000..79b5bfb233 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.alongsideanotherwebframework; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletProperties; + +import org.springframework.stereotype.Component; + +@Component +public class JerseyConfig extends ResourceConfig { + + public JerseyConfig() { + register(Endpoint.class); + property(ServletProperties.FILTER_FORWARD_ON_404, true); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java new file mode 100644 index 0000000000..47b236e959 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.springsecurity; + +class Endpoint { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java new file mode 100644 index 0000000000..7ff5a0c307 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.springsecurity; + +import java.util.Collections; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.stereotype.Component; + +@Component +public class JerseySetStatusOverSendErrorConfig extends ResourceConfig { + + public JerseySetStatusOverSendErrorConfig() { + register(Endpoint.class); + setProperties(Collections.singletonMap("jersey.config.server.response.setStatusOverSendError", true)); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyEndpoint.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyEndpoint.java new file mode 100644 index 0000000000..3dc270e22b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyEndpoint.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.web.servlet.jersey; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.springframework.stereotype.Component; + +@Component +@Path("/hello") +public class MyEndpoint { + + @GET + public String message() { + return "Hello"; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyJerseyConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyJerseyConfig.java new file mode 100644 index 0000000000..30d5678074 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyJerseyConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.web.servlet.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.stereotype.Component; + +@Component +public class MyJerseyConfig extends ResourceConfig { + + public MyJerseyConfig() { + register(MyEndpoint.class); + } + +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/build.gradle new file mode 100644 index 0000000000..5202fbe971 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/build.gradle @@ -0,0 +1,26 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building RESTful web applications using JAX-RS and Jersey. An alternative to spring-boot-starter-web" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-tomcat")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-validation")) + api("org.springframework:spring-web") + api("org.glassfish.jersey.containers:jersey-container-servlet-core") + api("org.glassfish.jersey.containers:jersey-container-servlet") + api("org.glassfish.jersey.core:jersey-server") + api("org.glassfish.jersey.ext:jersey-bean-validation") { + exclude group: "jakarta.el", module: "jakarta.el-api" + exclude group: "org.glassfish", module: "jakarta.el" + } + api("org.glassfish.jersey.ext:jersey-spring6") + api("org.glassfish.jersey.media:jersey-media-json-jackson") +} + +checkRuntimeClasspathForConflicts { + ignore { name -> name.startsWith("org/aopalliance/intercept/") } + ignore { name -> name.startsWith("org/aopalliance/aop/") } +} diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java index 56a4f057c7..a74742d124 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java @@ -90,6 +90,8 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework.web.servlet.DispatcherServlet"; + private static final String JERSEY_WEB_ENVIRONMENT_CLASS = "org.glassfish.jersey.server.ResourceConfig"; + private static final String ACTIVATE_SERVLET_LISTENER = "org.springframework.test." + "context.web.ServletTestExecutionListener.activateListener"; @@ -175,7 +177,8 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr private WebApplicationType deduceWebApplicationType() { if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null) - && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) { + && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null) + && !ClassUtils.isPresent(JERSEY_WEB_ENVIRONMENT_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : WEB_ENVIRONMENT_CLASSES) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java index 5f760509f2..82ae889035 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java @@ -55,9 +55,11 @@ public enum WebApplicationType { private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler"; + private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer"; + static WebApplicationType deduceFromClasspath() { - if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) - && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)) { + if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) + && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { @@ -75,6 +77,7 @@ public enum WebApplicationType { for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) { registerTypeIfPresent(servletIndicatorClass, classLoader, hints); } + registerTypeIfPresent(JERSEY_INDICATOR_CLASS, classLoader, hints); registerTypeIfPresent(WEBFLUX_INDICATOR_CLASS, classLoader, hints); registerTypeIfPresent(WEBMVC_INDICATOR_CLASS, classLoader, hints); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/build.gradle new file mode 100644 index 0000000000..f93f3fa6d5 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Jersey smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jersey")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-tomcat")) + + if (JavaVersion.current().java9Compatible) { + runtimeOnly("jakarta.xml.bind:jakarta.xml.bind-api") + } + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Endpoint.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Endpoint.java new file mode 100644 index 0000000000..80afef16d3 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Endpoint.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.springframework.stereotype.Component; + +@Component +@Path("/hello") +public class Endpoint { + + private final Service service; + + public Endpoint(Service service) { + this.service = service; + } + + @GET + public String message() { + return "Hello " + this.service.message(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/JerseyConfig.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/JerseyConfig.java new file mode 100644 index 0000000000..936558a32e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/JerseyConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.stereotype.Component; + +@Component +public class JerseyConfig extends ResourceConfig { + + public JerseyConfig() { + register(Endpoint.class); + register(ReverseEndpoint.class); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/ReverseEndpoint.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/ReverseEndpoint.java new file mode 100644 index 0000000000..3b0eafc294 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/ReverseEndpoint.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +import org.springframework.stereotype.Component; + +@Component +@Path("/reverse") +public class ReverseEndpoint { + + @GET + public String reverse(@QueryParam("input") @NotNull String input) { + return new StringBuilder(input).reverse().toString(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java new file mode 100644 index 0000000000..0027e21df5 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class SampleJerseyApplication extends SpringBootServletInitializer { + + public static void main(String[] args) { + new SampleJerseyApplication().configure(new SpringApplicationBuilder(SampleJerseyApplication.class)).run(args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Service.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Service.java new file mode 100644 index 0000000000..02b1b625c4 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Service.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class Service { + + @Value("${message:World}") + private String msg; + + public String message() { + return this.msg; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java new file mode 100644 index 0000000000..2e46f4f485 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "logging.level.root=debug") +abstract class AbstractJerseyApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity entity = this.restTemplate.getForEntity("/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void reverse() { + ResponseEntity entity = this.restTemplate.getForEntity("/reverse?input=olleh", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("hello"); + } + + @Test + void validation() { + ResponseEntity entity = this.restTemplate.getForEntity("/reverse", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void actuatorStatus() { + ResponseEntity entity = this.restTemplate.getForEntity("/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\"}"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java new file mode 100644 index 0000000000..219c9c3caf --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; +import smoketest.jersey.AbstractJerseyManagementPortTests.ResourceConfigConfiguration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Base class for integration tests for Jersey using separate management and main service + * ports. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "management.server.port=0") +@Import(ResourceConfigConfiguration.class) +class AbstractJerseyManagementPortTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void resourceShouldBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate.getForEntity("http://localhost:" + this.port + "/test", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("test"); + } + + @Test + void resourceShouldNotBeAvailableOnManagementPort() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.managementPort + "/test", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void actuatorShouldBeAvailableOnManagementPort() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void actuatorShouldNotBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.port + "/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @TestConfiguration + static class ResourceConfigConfiguration { + + @Bean + ResourceConfigCustomizer customizer() { + return new ResourceConfigCustomizer() { + @Override + public void customize(ResourceConfig config) { + config.register(TestEndpoint.class); + } + }; + } + + @Path("/test") + public static class TestEndpoint { + + @GET + public String test() { + return "test"; + } + + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java new file mode 100644 index 0000000000..5843f0a0fc --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with custom + * application path. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "spring.jersey.application-path=/app" }) +class JerseyApplicationPathAndManagementPortTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void applicationPathShouldNotAffectActuators() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java new file mode 100644 index 0000000000..e5e332e601 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with empty base path + * for endpoints. + * + * @author HaiTao Zhang + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "management.endpoints.web.base-path=/" }) +class JerseyDifferentPortSampleActuatorApplicationTests { + + @LocalManagementPort + private int managementPort; + + @Test + void linksEndpointShouldBeAvailable() { + ResponseEntity entity = new TestRestTemplate("user", getPassword()) + .getForEntity("http://localhost:" + this.managementPort + "/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"_links\""); + } + + private String getPassword() { + return "password"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterApplicationTests.java new file mode 100644 index 0000000000..c3e379fde6 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterApplicationTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.springframework.test.context.TestPropertySource; + +/** + * Smoke tests for Jersey configured as a Filter. + * + * @author Andy Wilkinson + */ +@TestPropertySource(properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) +class JerseyFilterApplicationTests extends AbstractJerseyApplicationTests { + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterManagementPortTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterManagementPortTests.java new file mode 100644 index 0000000000..3581a798d5 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterManagementPortTests.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +import org.springframework.test.context.TestPropertySource; + +/** + * Integration tests for Jersey configured as a Servlet using separate management and main + * service ports. + * + * @author Andy Wilkinson + */ +@TestPropertySource(properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) +class JerseyFilterManagementPortTests extends AbstractJerseyManagementPortTests { + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletApplicationTests.java new file mode 100644 index 0000000000..4e637c26c7 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletApplicationTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +/** + * Smoke tests for Jersey configured as a Servlet. + * + * @author Andy Wilkinson + */ +class JerseyServletApplicationTests extends AbstractJerseyApplicationTests { + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletManagementPortTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletManagementPortTests.java new file mode 100644 index 0000000000..2e9d36ee98 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletManagementPortTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.jersey; + +/** + * Integration tests for Jersey configured as a Servlet using separate management and main + * service ports. + * + * @author Andy Wilkinson + */ +class JerseyServletManagementPortTests extends AbstractJerseyManagementPortTests { + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/build.gradle new file mode 100644 index 0000000000..45b5711180 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot secure Jersey smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jersey")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-security")) + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java new file mode 100644 index 0000000000..fa3b9cca9e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.springframework.stereotype.Component; + +@Component +@Path("/hello") +public class Endpoint { + + private final Service service; + + public Endpoint(Service service) { + this.service = service; + } + + @GET + public String message() { + return "Hello " + this.service.message(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java new file mode 100644 index 0000000000..de42cbf257 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.stereotype.Component; + +@Component +public class JerseyConfig extends ResourceConfig { + + public JerseyConfig() { + register(Endpoint.class); + register(ReverseEndpoint.class); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java new file mode 100644 index 0000000000..5d8b0382e7 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +import org.springframework.stereotype.Component; + +@Component +@Path("/reverse") +public class ReverseEndpoint { + + @GET + public String reverse(@QueryParam("input") @NotNull String input) { + return new StringBuilder(input).reverse().toString(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java new file mode 100644 index 0000000000..dd49b7c066 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import java.io.IOException; +import java.util.function.Supplier; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.endpoint.web.EndpointServlet; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SampleSecureJerseyApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleSecureJerseyApplication.class, args); + } + + @Bean + TestServletEndpoint servletEndpoint() { + return new TestServletEndpoint(); + } + + @ServletEndpoint(id = "se1") + static class TestServletEndpoint implements Supplier { + + @Override + public EndpointServlet get() { + return new EndpointServlet(ExampleServlet.class); + } + + } + + static class ExampleServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java new file mode 100644 index 0000000000..83f0e7d804 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; +import org.springframework.boot.actuate.web.mappings.MappingsEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfiguration { + + @SuppressWarnings("deprecation") + @Bean + public InMemoryUserDetailsManager inMemoryUserDetailsManager() { + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder().username("user").password("password").authorities("ROLE_USER") + .build(), + User.withDefaultPasswordEncoder().username("admin").password("admin") + .authorities("ROLE_ACTUATOR", "ROLE_USER").build()); + } + + @Bean + SecurityFilterChain configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests() + .requestMatchers(EndpointRequest.to("health")).permitAll() + .requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR") + .antMatchers("/**").hasRole("USER") + .and() + .httpBasic(); + return http.build(); + // @formatter:on + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java new file mode 100644 index 0000000000..bb5ae903f7 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class Service { + + @Value("${message:World}") + private String msg; + + public String message() { + return this.msg; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties new file mode 100644 index 0000000000..5d894fac2c --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties @@ -0,0 +1,4 @@ +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java new file mode 100644 index 0000000000..515ab8c5bc --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Abstract base class for actuator tests with custom security. + * + * @author Madhura Bhave + */ +abstract class AbstractJerseySecureTests { + + abstract String getPath(); + + abstract String getManagementPath(); + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void helloEndpointIsSecure() { + ResponseEntity entity = restTemplate().getForEntity(getPath() + "/hello", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorInsecureEndpoint() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/health/diskSpace", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + } + + @Test + void actuatorLinksWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorLinksWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void actuatorLinksWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + adminRestTemplate().getForEntity(getManagementPath() + "/actuator/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void actuatorSecureEndpointWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/env", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void actuatorSecureEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/env", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void actuatorSecureEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/env", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + entity = adminRestTemplate().getForEntity( + getManagementPath() + "/actuator/env/management.endpoints.web.exposure.include", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void secureServletEndpointWithAnonymous() { + ResponseEntity entity = restTemplate().getForEntity(getManagementPath() + "/actuator/se1", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = restTemplate().getForEntity(getManagementPath() + "/actuator/se1/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void secureServletEndpointWithUnauthorizedUser() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/se1", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/se1/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void secureServletEndpointWithAuthorizedUser() { + ResponseEntity entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/se1", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + entity = adminRestTemplate().getForEntity(getManagementPath() + "/actuator/se1/list", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void actuatorExcludedFromEndpointRequestMatcher() { + ResponseEntity entity = userRestTemplate().getForEntity(getManagementPath() + "/actuator/mappings", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + TestRestTemplate restTemplate() { + return this.testRestTemplate; + } + + TestRestTemplate adminRestTemplate() { + return this.testRestTemplate.withBasicAuth("admin", "admin"); + } + + TestRestTemplate userRestTemplate() { + return this.testRestTemplate.withBasicAuth("user", "password"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java new file mode 100644 index 0000000000..95badda877 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +/** + * Integration tests for actuator endpoints with custom application path. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.jersey.application-path=/example") + +class CustomApplicationPathActuatorTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @Override + String getPath() { + return "http://localhost:" + this.port + "/example"; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.port + "/example"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java new file mode 100644 index 0000000000..2b708a4767 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +/** + * Integration tests for actuator endpoints with custom security configuration. + * + * @author Madhura Bhave + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class JerseySecureApplicationTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @Override + String getPath() { + return "http://localhost:" + this.port; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.port; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java new file mode 100644 index 0000000000..633f48ac03 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with custom management + * context path. + * + * @author Dave Syer + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "management.server.base-path=/management" }) +class ManagementPortAndPathJerseyApplicationTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Test + void testMissing() { + ResponseEntity entity = new TestRestTemplate("admin", "admin") + .getForEntity("http://localhost:" + this.managementPort + "/management/actuator/missing", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Override + String getPath() { + return "http://localhost:" + this.port; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.managementPort + "/management"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java new file mode 100644 index 0000000000..355f12b98d --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.secure.jersey; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports with custom + * application path. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "management.server.port=0", "spring.jersey.application-path=/example" }) +class ManagementPortCustomApplicationPathJerseyTests extends AbstractJerseySecureTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Test + void actuatorPathOnMainPortShouldNotMatch() { + ResponseEntity entity = new TestRestTemplate() + .getForEntity("http://localhost:" + this.port + "/example/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Override + String getPath() { + return "http://localhost:" + this.port + "/example"; + } + + @Override + String getManagementPath() { + return "http://localhost:" + this.managementPort; + } + +}