diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle index bbc3250f04b..01391286d45 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -92,6 +92,7 @@ def dependenciesOf(String version) { "spring-boot-jackson2", "spring-boot-jdbc", "spring-boot-jdbc-test", + "spring-boot-jersey", "spring-boot-jetty", "spring-boot-jms", "spring-boot-jooq", diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index ec59f1ed460..fb4957d1606 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -165,6 +165,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") + 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/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc index 21118daa779..8406a8860c1 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/documentation.adoc @@ -64,7 +64,7 @@ xref:reference:features/index.adoc[The following content is for you]: If you develop Spring Boot web applications, take a look at the following content: -* *Servlet Web Applications:* xref:reference:web/servlet.adoc[Spring MVC, Embedded Servlet Containers] +* *Servlet Web Applications:* xref:reference:web/servlet.adoc[Spring MVC, Jersey, Embedded Servlet Containers] * *Reactive Web Applications:* xref:reference:web/reactive.adoc[Spring Webflux, Embedded Servlet Containers] * *Graceful Shutdown:* xref:reference:web/graceful-shutdown.adoc[Graceful Shutdown] * *Spring Security:* xref:reference:web/spring-security.adoc[Default Security Configuration, Auto-configuration for OAuth2, SAML] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index dcf392dd8f8..f1c346d489d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -550,6 +550,12 @@ * xref:how-to:index.adoc#howto[#howto] * xref:how-to:index.adoc[#howto] * xref:how-to:index.adoc[howto] +* xref:how-to:jersey.adoc#howto.jersey.alongside-another-web-framework[#howto-jersey-alongside-another-web-framework] +* xref:how-to:jersey.adoc#howto.jersey.alongside-another-web-framework[#howto.jersey.alongside-another-web-framework] +* xref:how-to:jersey.adoc#howto.jersey.spring-security[#howto-jersey-spring-security] +* xref:how-to:jersey.adoc#howto.jersey.spring-security[#howto.jersey.spring-security] +* xref:how-to:jersey.adoc#howto.jersey[#howto-jersey] +* xref:how-to:jersey.adoc#howto.jersey[#howto.jersey] * xref:how-to:logging.adoc#howto.logging.log4j.composite-configuration[#howto.logging.log4j.composite-configuration] * xref:how-to:logging.adoc#howto.logging.log4j.yaml-or-json-config[#howto-configure-log4j-for-logging-yaml-or-json-config] * xref:how-to:logging.adoc#howto.logging.log4j.yaml-or-json-config[#howto.logging.log4j.yaml-or-json-config] @@ -1124,6 +1130,8 @@ * xref:reference:actuator/metrics.adoc#actuator.metrics.supported.http-clients[#production-ready-metrics-http-clients] * xref:reference:actuator/metrics.adoc#actuator.metrics.supported.jdbc[#actuator.metrics.supported.jdbc] * xref:reference:actuator/metrics.adoc#actuator.metrics.supported.jdbc[#production-ready-metrics-jdbc] +* xref:reference:actuator/metrics.adoc#actuator.metrics.supported.jersey[#actuator.metrics.supported.jersey] +* xref:reference:actuator/metrics.adoc#actuator.metrics.supported.jersey[#production-ready-metrics-jersey-server] * xref:reference:actuator/metrics.adoc#actuator.metrics.supported.jetty[#actuator.metrics.supported.jetty] * xref:reference:actuator/metrics.adoc#actuator.metrics.supported.jms[#actuator.metrics.supported.jms] * xref:reference:actuator/metrics.adoc#actuator.metrics.supported.jvm[#actuator.metrics.supported.jvm] @@ -2097,6 +2105,9 @@ * xref:reference:web/servlet.adoc#web.servlet.embedded-container[#boot-features-embedded-container] * xref:reference:web/servlet.adoc#web.servlet.embedded-container[#features.developing-web-applications.embedded-container] * xref:reference:web/servlet.adoc#web.servlet.embedded-container[#web.servlet.embedded-container] +* xref:reference:web/servlet.adoc#web.servlet.jersey[#boot-features-jersey] +* xref:reference:web/servlet.adoc#web.servlet.jersey[#features.developing-web-applications.jersey] +* xref:reference:web/servlet.adoc#web.servlet.jersey[#web.servlet.jersey] * xref:reference:web/servlet.adoc#web.servlet.spring-mvc.auto-configuration[#boot-features-spring-mvc-auto-configuration] * xref:reference:web/servlet.adoc#web.servlet.spring-mvc.auto-configuration[#features.developing-web-applications.spring-mvc.auto-configuration] * xref:reference:web/servlet.adoc#web.servlet.spring-mvc.auto-configuration[#web.servlet.spring-mvc.auto-configuration] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/jersey.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/jersey.adoc new file mode 100644 index 00000000000..ba6de634086 --- /dev/null +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/jersey.adoc @@ -0,0 +1,26 @@ +[[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 javadoc:org.glassfish.jersey.server.ResourceConfig[] bean, as shown in the following example: + +include-code::JerseySetStatusOverSendErrorConfig[] + + + +[[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 javadoc:org.glassfish.jersey.server.ResourceConfig[] to forward requests that would have resulted in a 404, as shown in the following example. + +include-code::JerseyConfig[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/partials/nav-how-to.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/partials/nav-how-to.adoc index 0c3371c0cf2..20a540866dd 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/partials/nav-how-to.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/partials/nav-how-to.adoc @@ -4,6 +4,7 @@ ** xref:how-to:properties-and-configuration.adoc[] ** xref:how-to:webserver.adoc[] ** xref:how-to:spring-mvc.adoc[] +** xref:how-to:jersey.adoc[] ** xref:how-to:http-clients.adoc[] ** xref:how-to:logging.adoc[] ** xref:how-to:data-access.adoc[] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc index e51d34cd951..c339cc8b43f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc @@ -95,7 +95,7 @@ Subject to xref:actuator/endpoints.adoc#actuator.endpoints.sanitization[sanitiza | 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"] |=== @@ -388,7 +388,8 @@ TIP: See javadoc:org.springframework.boot.actuate.autoconfigure.endpoint.web.Cor == Implementing Custom Endpoints If you add a javadoc:org.springframework.context.annotation.Bean[format=annotation] annotated with javadoc:org.springframework.boot.actuate.endpoint.annotation.Endpoint[format=annotation], any methods annotated with javadoc:org.springframework.boot.actuate.endpoint.annotation.ReadOperation[format=annotation], javadoc:org.springframework.boot.actuate.endpoint.annotation.WriteOperation[format=annotation], or javadoc:org.springframework.boot.actuate.endpoint.annotation.DeleteOperation[format=annotation] 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: @@ -451,7 +452,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 javadoc:org.springframework.boot.actuate.endpoint.annotation.Endpoint[format=annotation], javadoc:org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint[format=annotation], or javadoc:org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension[format=annotation] are automatically exposed over HTTP using Spring MVC or Spring WebFlux. +Operations on an javadoc:org.springframework.boot.actuate.endpoint.annotation.Endpoint[format=annotation], javadoc:org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint[format=annotation], or javadoc:org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension[format=annotation] are automatically exposed over HTTP using Jersey, Spring MVC, or Spring WebFlux. +If both Jersey and Spring MVC are available, Spring MVC is used. @@ -539,6 +541,8 @@ If an operation is invoked without a required parameter or with a parameter that You can use an HTTP range request to request part of an HTTP resource. When using Spring MVC or Spring Web Flux, operations that return a javadoc:org.springframework.core.io.Resource[] automatically support range requests. +NOTE: Range requests are not supported when using Jersey. + [[actuator.endpoints.implementing-custom.web.security]] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index ea2ba34ef45..22711b6a05a 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -765,6 +765,39 @@ Applications can opt in and record exceptions by xref:web/reactive.adoc#web.reac +[[actuator.metrics.supported.jersey]] +=== Jersey Server Metrics + +Auto-configuration enables the instrumentation of all requests handled by the Jersey JAX-RS implementation. +By default, metrics are generated with the name, `http.server.requests`. +You can customize the name by setting the configprop:management.observations.http.server.requests.name[] property. + +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 javadoc:org.springframework.context.annotation.Bean[format=annotation] that implements javadoc:io.micrometer.core.instrument.binder.jersey.server.JerseyObservationConvention[]. + + + [[actuator.metrics.supported.ssl]] === SSL Bundle Metrics diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/monitoring.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/monitoring.adoc index c8c0d241711..aed5a2acf0e 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/monitoring.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/monitoring.adoc @@ -5,9 +5,12 @@ 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 xref:api:rest/actuator/index.adoc[API documentation]. +Jackson 3 should be used for Spring MVC and Spring WebFlux. +Jersey does not yet have a Jackson 3 module, so you will need to use Jackson 2. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc index 4a5d1020e52..6936495b562 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc @@ -1,7 +1,7 @@ [[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. @@ -495,6 +495,43 @@ See xref:io/rest-client.adoc#io.rest-client.apiversioning[] for details. +[[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 javadoc:jakarta.servlet.Servlet[] or javadoc:jakarta.servlet.Filter[] as a javadoc:org.springframework.context.annotation.Bean[format=annotation] 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 javadoc:org.springframework.context.annotation.Bean[format=annotation] of type javadoc:org.glassfish.jersey.server.ResourceConfig[] in which you register all the endpoints, as shown in the following example: + +include-code::MyJerseyConfig[] + +WARNING: Jersey's support for scanning executable archives is rather limited. +For example, it cannot scan for endpoints in a package found in a xref:how-to:deployment/installing.adoc[fully executable jar file] 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 javadoc:org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer[]. + +All the registered endpoints should be a javadoc:org.springframework.stereotype.Component[format=annotation] with HTTP resource annotations (`@GET` and others), as shown in the following example: + +include-code::MyEndpoint[] + +Since the javadoc:org.springframework.boot.actuate.endpoint.annotation.Endpoint[format=annotation] is a Spring javadoc:org.springframework.stereotype.Component[format=annotation], its lifecycle is managed by Spring and you can use the javadoc:org.springframework.beans.factory.annotation.Autowired[format=annotation] annotation to inject dependencies and use the javadoc:org.springframework.beans.factory.annotation.Value[format=annotation] annotation to inject external configuration. +By default, the Jersey servlet is registered and mapped to `/*`. +You can change the mapping by adding javadoc:jakarta.ws.rs.ApplicationPath[format=annotation] to your javadoc:org.glassfish.jersey.server.ResourceConfig[]. + +By default, Jersey is set up as a servlet in a javadoc:org.springframework.context.annotation.Bean[format=annotation] of type javadoc:org.springframework.boot.web.servlet.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 javadoc:org.springframework.context.annotation.Bean[format=annotation] to replace or override is `jerseyFilterRegistration`). +The filter has an javadoc:org.springframework.core.annotation.Order[format=annotation], 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 diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java new file mode 100644 index 00000000000..69d982833d6 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-present 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/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java new file mode 100644 index 00000000000..2363f2468be --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present 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/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java new file mode 100644 index 00000000000..5f3fa9bcf3e --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-present 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/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java new file mode 100644 index 00000000000..3521a872f38 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present 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/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyEndpoint.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyEndpoint.java new file mode 100644 index 00000000000..376cab6ac95 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyEndpoint.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present 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/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyJerseyConfig.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyJerseyConfig.java new file mode 100644 index 00000000000..b227fdee420 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/jersey/MyJerseyConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present 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/integration-test/spring-boot-actuator-integration-tests/build.gradle b/integration-test/spring-boot-actuator-integration-tests/build.gradle index 44524ba74ef..9d98ba3c79e 100644 --- a/integration-test/spring-boot-actuator-integration-tests/build.gradle +++ b/integration-test/spring-boot-actuator-integration-tests/build.gradle @@ -29,6 +29,7 @@ dependencies { testImplementation(project(":module:spring-boot-health")) testImplementation(project(":module:spring-boot-http-converter")) testImplementation(project(":module:spring-boot-jackson")) + testImplementation(project(":module:spring-boot-jersey")) testImplementation(project(":module:spring-boot-micrometer-metrics")) testImplementation(project(":module:spring-boot-reactor-netty")) testImplementation(project(":core:spring-boot-test")) @@ -37,6 +38,7 @@ dependencies { testImplementation(project(":module:spring-boot-web-server")) testImplementation(project(":module:spring-boot-webflux")) testImplementation(project(":module:spring-boot-webmvc")) + testImplementation(testFixtures(project(":module:spring-boot-jersey"))) testImplementation(testFixtures(project(":module:spring-boot-webflux"))) testImplementation(testFixtures(project(":module:spring-boot-webmvc"))) testImplementation("io.micrometer:micrometer-registry-prometheus") diff --git a/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java b/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java new file mode 100644 index 00000000000..42e73b807d7 --- /dev/null +++ b/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-present 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.jersey.actuate.endpoint.web.JerseyEndpointResourceFactory; +import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +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 + */ +class JerseyWebEndpointIntegrationTests + extends AbstractWebEndpointIntegrationTests { + + 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/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java b/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java index 180f3ca6605..4cd071eacee 100644 --- a/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java +++ b/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java @@ -46,10 +46,15 @@ public @interface WebEndpointTest { * The infrastructure against which the test should run. * @return the infrastructure to run the tests against */ - Infrastructure[] infrastructure() default { Infrastructure.MVC, Infrastructure.WEBFLUX }; + Infrastructure[] infrastructure() default { Infrastructure.JERSEY, Infrastructure.MVC, Infrastructure.WEBFLUX }; enum Infrastructure { + /** + * Actuator running on the Jersey-based infrastructure. + */ + JERSEY("Jersey"), + /** * Actuator running on the WebMVC-based infrastructure. */ diff --git a/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java b/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java index 41faa44b37a..2a5d4502022 100644 --- a/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java +++ b/module/spring-boot-actuator/src/testFixtures/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java @@ -77,8 +77,8 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation .findFirst() .ifPresent((provider) -> this.infrastructures.put(infrastructure, provider.getInfrastructureConfiguration(infrastructure)))); - this.contextFactories = Map.of(Infrastructure.MVC, this::createWebMvcContext, Infrastructure.WEBFLUX, - this::createWebFluxContext); + this.contextFactories = Map.of(Infrastructure.JERSEY, this::createJerseyContext, Infrastructure.MVC, + this::createWebMvcContext, Infrastructure.WEBFLUX, this::createWebFluxContext); } @Override @@ -100,6 +100,14 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation return infrastructure.createInvocationContext(this.contextFactories.get(infrastructure)); } + private ConfigurableApplicationContext createJerseyContext(List> classes) { + AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); + classes.addAll(getEndpointInfrastructureConfiguration(Infrastructure.JERSEY)); + context.register(ClassUtils.toClassArray(classes)); + context.refresh(); + return context; + } + private ConfigurableApplicationContext createWebMvcContext(List> classes) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); classes.addAll(getEndpointInfrastructureConfiguration(Infrastructure.MVC)); diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 6bf7abd57a7..e6827186ce4 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -136,6 +136,9 @@ dependencies { api(project(":module:spring-boot-jdbc")) { transitive = false } + api(project(":module:spring-boot-jersey")) { + transitive = false + } api(project(":module:spring-boot-jetty")) { transitive = false } diff --git a/module/spring-boot-cache/build.gradle b/module/spring-boot-cache/build.gradle index efe08e2c48d..52e900f9404 100644 --- a/module/spring-boot-cache/build.gradle +++ b/module/spring-boot-cache/build.gradle @@ -62,6 +62,7 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) + testImplementation(testFixtures(project(":module:spring-boot-jersey"))) testImplementation(testFixtures(project(":module:spring-boot-webmvc"))) testImplementation(testFixtures(project(":module:spring-boot-webflux"))) diff --git a/module/spring-boot-integration/build.gradle b/module/spring-boot-integration/build.gradle index dc1015e58b3..a56c78c8c5b 100644 --- a/module/spring-boot-integration/build.gradle +++ b/module/spring-boot-integration/build.gradle @@ -42,6 +42,7 @@ dependencies { testImplementation(project(":module:spring-boot-rsocket")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":module:spring-boot-sql"))) + testImplementation(testFixtures(project(":module:spring-boot-jersey"))) testImplementation(testFixtures(project(":module:spring-boot-webflux"))) testImplementation(testFixtures(project(":module:spring-boot-webmvc"))) testImplementation("org.springframework:spring-web") diff --git a/module/spring-boot-jersey/build.gradle b/module/spring-boot-jersey/build.gradle new file mode 100644 index 00000000000..5b171d2752a --- /dev/null +++ b/module/spring-boot-jersey/build.gradle @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id "java-library" + id "java-test-fixtures" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Jersey" + +dependencies { + api(project(":module:spring-boot-servlet")) + api("org.glassfish.jersey.containers:jersey-container-servlet") + api("org.glassfish.jersey.core:jersey-server") + api("org.glassfish.jersey.ext:jersey-spring6") + api("org.glassfish.jersey.media:jersey-media-json-jackson") + + compileOnly("jakarta.servlet:jakarta.servlet-api") + compileOnly("com.google.code.findbugs:jsr305") + + implementation("org.springframework:spring-web") + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":module:spring-boot-actuator-autoconfigure")) + optional(project(":module:spring-boot-health")) + optional(project(":module:spring-boot-jackson2")) + optional(project(":module:spring-boot-micrometer-metrics")) + optional(project(":module:spring-boot-micrometer-observation")) + optional("io.projectreactor:reactor-core") + optional("org.glassfish.jersey.ext:jersey-micrometer") + + testFixturesApi(testFixtures(project(":module:spring-boot-actuator"))) + testFixturesImplementation(project(":module:spring-boot-tomcat")) + testFixturesRuntimeOnly(project(":module:spring-boot-jackson2")) + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":module:spring-boot-restclient")) + testImplementation(project(":module:spring-boot-resttestclient")) + testImplementation(project(":module:spring-boot-tomcat")) + testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation(testFixtures(project(":module:spring-boot-actuator-autoconfigure"))) + testImplementation("jakarta.servlet:jakarta.servlet-api") + testImplementation("org.springframework:spring-webflux") + + testRuntimeOnly("ch.qos.logback:logback-classic") +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyEndpointResourceFactory.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyEndpointResourceFactory.java new file mode 100644 index 00000000000..cee78158027 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyEndpointResourceFactory.java @@ -0,0 +1,362 @@ +/* + * Copyright 2012-present 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.jersey.actuate.endpoint.web; + +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 org.jspecify.annotations.Nullable; +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.OperationResponseBody; +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.core.ParameterizedTypeReference; +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 4.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, @Nullable WebServerNamespace serverNamespace, + @Nullable 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 @Nullable WebServerNamespace serverNamespace; + + private final @Nullable JerseyRemainingPathSegmentProvider remainingPathSegmentProvider; + + private OperationInflector(WebOperation operation, boolean readBody, + @Nullable WebServerNamespace serverNamespace, + @Nullable 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, + new ParameterizedTypeReference>() { + }.getType()); + 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 @Nullable 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(@Nullable 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(@Nullable 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(); + } + if (!(response instanceof WebEndpointResponse webEndpointResponse)) { + return Response.status(Status.OK).entity(convertIfNecessary(response)).build(); + } + return Response.status(webEndpointResponse.getStatus()) + .header("Content-Type", webEndpointResponse.getContentType()) + .entity(convertIfNecessary(webEndpointResponse.getBody())) + .build(); + } + + private @Nullable Object convertIfNecessary(@Nullable Object body) { + for (Function<@Nullable Object, @Nullable Object> 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<@Nullable Object, @Nullable Object> { + + @Override + public @Nullable Object apply(@Nullable 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<@Nullable Object, @Nullable Object> { + + @Override + public @Nullable Object apply(@Nullable 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()); + Map> entity = OperationResponseBody.of(Collections.singletonMap("_links", links)); + return Response.ok(entity).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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyHealthEndpointAdditionalPathResourceFactory.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyHealthEndpointAdditionalPathResourceFactory.java new file mode 100644 index 00000000000..8fae6aa8133 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyHealthEndpointAdditionalPathResourceFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-present 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.jersey.actuate.endpoint.web; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.glassfish.jersey.server.model.Resource; + +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +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.health.actuate.endpoint.AdditionalHealthEndpointPath; +import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroup; +import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; + +/** + * A factory for creating Jersey {@link Resource Resources} for health groups with + * additional path. + * + * @author Madhura Bhave + * @since 2.6.0 + */ +public final class JerseyHealthEndpointAdditionalPathResourceFactory { + + private final JerseyEndpointResourceFactory delegate = new JerseyEndpointResourceFactory(); + + private final Set groups; + + private final WebServerNamespace serverNamespace; + + public JerseyHealthEndpointAdditionalPathResourceFactory(WebServerNamespace serverNamespace, + HealthEndpointGroups groups) { + this.serverNamespace = serverNamespace; + this.groups = groups.getAllWithAdditionalPath(serverNamespace); + } + + public Collection createEndpointResources(EndpointMapping endpointMapping, + Collection endpoints) { + return endpoints.stream() + .flatMap((endpoint) -> endpoint.getOperations().stream()) + .flatMap((operation) -> createResources(endpointMapping, operation)) + .toList(); + } + + private Stream createResources(EndpointMapping endpointMapping, WebOperation operation) { + WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate(); + String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable(); + if (matchAllRemainingPathSegmentsVariable != null) { + List resources = new ArrayList<>(); + for (HealthEndpointGroup group : this.groups) { + AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath(); + if (additionalPath != null) { + resources.add(this.delegate.getResource(endpointMapping, operation, requestPredicate, + additionalPath.getValue(), this.serverNamespace, + (data, pathSegmentsVariable) -> data.getUriInfo().getPath())); + } + } + return resources.stream(); + } + return Stream.empty(); + } + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyRemainingPathSegmentProvider.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyRemainingPathSegmentProvider.java new file mode 100644 index 00000000000..90a2704f10e --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/JerseyRemainingPathSegmentProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present 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.jersey.actuate.endpoint.web; + +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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/package-info.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/package-info.java new file mode 100644 index 00000000000..776b9ebe5f2 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/actuate/endpoint/web/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present 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. + */ +@NullMarked +package org.springframework.boot.jersey.actuate.endpoint.web; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/DefaultJerseyApplicationPath.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/DefaultJerseyApplicationPath.java new file mode 100644 index 00000000000..e064d8a3f57 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/DefaultJerseyApplicationPath.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +import jakarta.ws.rs.ApplicationPath; +import org.glassfish.jersey.server.ResourceConfig; +import org.jspecify.annotations.Nullable; + +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 4.0.0 + */ +public class DefaultJerseyApplicationPath implements JerseyApplicationPath { + + private final @Nullable String applicationPath; + + private final ResourceConfig config; + + public DefaultJerseyApplicationPath(@Nullable 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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyApplicationPath.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyApplicationPath.java new file mode 100644 index 00000000000..721fd551a97 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyApplicationPath.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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 4.0.0 + */ +@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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfiguration.java new file mode 100644 index 00000000000..ad0fb9308fe --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfiguration.java @@ -0,0 +1,233 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +import java.util.Collections; +import java.util.EnumSet; + +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.ConditionalOnMissingFilterBean; +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.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 4.0.0 + */ +@AutoConfiguration(afterName = { "org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" }) +@ConditionalOnClass({ SpringComponentProvider.class, ServletRegistration.class }) +@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig") +@ConditionalOnWebApplication(type = Type.SERVLET) +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@EnableConfigurationProperties(JerseyProperties.class) +public final class JerseyAutoConfiguration implements ServletContextAware { + + private static final Log logger = LogFactory.getLog(JerseyAutoConfiguration.class); + + private final JerseyProperties jersey; + + private final ResourceConfig config; + + JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config, + ObjectProvider customizers) { + this.jersey = jersey; + this.config = config; + customizers.orderedStream().forEach((customizer) -> customizer.customize(this.config)); + } + + @Bean + @ConditionalOnMissingFilterBean + FilterRegistrationBean requestContextFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new RequestContextFilter()); + registration.setOrder(this.jersey.getFilter().getOrder() - 1); + registration.setName("requestContextFilter"); + return registration; + } + + @Bean + @ConditionalOnMissingBean + JerseyApplicationPath jerseyApplicationPath() { + return new DefaultJerseyApplicationPath(this.jersey.getApplicationPath(), this.config); + } + + @Bean + @ConditionalOnMissingBean(name = "jerseyFilterRegistration") + @ConditionalOnProperty(name = "spring.jersey.type", havingValue = "filter") + 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(name = "spring.jersey.type", havingValue = "servlet", matchIfMissing = true) + 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()); + registration.setIgnoreRegistrationFailure(true); + 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 { + if (ClassUtils.isPresent("org.glassfish.jersey.server.spring.SpringWebApplicationInitializer", + getClass().getClassLoader())) { + // 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(com.fasterxml.jackson.databind.ObjectMapper.class) + static class Jackson2ResourceConfigCustomizerConfiguration { + + @Bean + ResourceConfigCustomizer jacksonResourceConfigCustomizer( + com.fasterxml.jackson.databind.ObjectMapper objectMapper) { + return (ResourceConfig config) -> { + config.register(JacksonFeature.class); + config.register(new ObjectMapperContextResolver(objectMapper), ContextResolver.class); + }; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector.class, + XmlElement.class }) + static class JaxbJackson2ObjectMapperCustomizerConfiguration { + + @Autowired + void addJaxbAnnotationIntrospector(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { + com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector jaxbAnnotationIntrospector = new com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector( + objectMapper.getTypeFactory()); + objectMapper.setAnnotationIntrospectors( + createPair(objectMapper.getSerializationConfig(), jaxbAnnotationIntrospector), + createPair(objectMapper.getDeserializationConfig(), jaxbAnnotationIntrospector)); + } + + private com.fasterxml.jackson.databind.AnnotationIntrospector createPair( + com.fasterxml.jackson.databind.cfg.MapperConfig config, + com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector jaxbAnnotationIntrospector) { + return com.fasterxml.jackson.databind.AnnotationIntrospector.pair(config.getAnnotationIntrospector(), + jaxbAnnotationIntrospector); + } + + } + + private static final class ObjectMapperContextResolver + implements ContextResolver { + + private final com.fasterxml.jackson.databind.ObjectMapper objectMapper; + + private ObjectMapperContextResolver(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public com.fasterxml.jackson.databind.ObjectMapper getContext(Class type) { + return this.objectMapper; + } + + } + + } + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyProperties.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyProperties.java new file mode 100644 index 00000000000..2a858ad353a --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/JerseyProperties.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +import java.util.HashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for Jersey. + * + * @author Dave Syer + * @author Eddú Meléndez + * @author Stephane Nicoll + * @since 4.0.0 + */ +@ConfigurationProperties("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 @Nullable 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 @Nullable String getApplicationPath() { + return this.applicationPath; + } + + public void setApplicationPath(@Nullable 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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/ResourceConfigCustomizer.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/ResourceConfigCustomizer.java new file mode 100644 index 00000000000..050798a2d7d --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/ResourceConfigCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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 4.0.0 + */ +@FunctionalInterface +public interface ResourceConfigCustomizer { + + /** + * Customize the resource config. + * @param config the {@link ResourceConfig} to customize + */ + void customize(ResourceConfig config); + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/HealthEndpointJerseyExtensionAutoConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/HealthEndpointJerseyExtensionAutoConfiguration.java new file mode 100644 index 00000000000..43adf70e672 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/HealthEndpointJerseyExtensionAutoConfiguration.java @@ -0,0 +1,148 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.endpoint.web; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.servlet.ServletContainer; +import org.jspecify.annotations.Nullable; + +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.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +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.context.properties.EnableConfigurationProperties; +import org.springframework.boot.health.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; +import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointAutoConfiguration; +import org.springframework.boot.jersey.actuate.endpoint.web.JerseyHealthEndpointAdditionalPathResourceFactory; +import org.springframework.boot.jersey.autoconfigure.DefaultJerseyApplicationPath; +import org.springframework.boot.jersey.autoconfigure.JerseyApplicationPath; +import org.springframework.boot.jersey.autoconfigure.JerseyProperties; +import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Auto-Configuration for {@link HealthEndpoint} Jersey extension. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 4.0.0 + * @see HealthEndpointAutoConfiguration + */ +@AutoConfiguration +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnBean(HealthEndpoint.class) +@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) +@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") +public final class HealthEndpointJerseyExtensionAutoConfiguration { + + @Bean + JerseyAdditionalHealthEndpointPathsResourcesRegistrar jerseyAdditionalHealthEndpointPathsResourcesRegistrar( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) { + ExposableWebEndpoint health = getHealthEndpoint(webEndpointsSupplier); + return new JerseyAdditionalHealthEndpointPathsResourcesRegistrar(health, healthEndpointGroups); + } + + private static @Nullable ExposableWebEndpoint getHealthEndpoint(WebEndpointsSupplier webEndpointsSupplier) { + Collection webEndpoints = webEndpointsSupplier.getEndpoints(); + return webEndpoints.stream() + .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) + .findFirst() + .orElse(null); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(ResourceConfig.class) + @EnableConfigurationProperties(JerseyProperties.class) + static class JerseyInfrastructureConfiguration { + + @Bean + @ConditionalOnMissingBean + 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 @Nullable ExposableWebEndpoint endpoint; + + private final HealthEndpointGroups groups; + + JerseyAdditionalHealthEndpointPathsResourcesRegistrar(@Nullable 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, + (this.endpoint != null) ? Collections.singletonList(this.endpoint) : Collections.emptyList()) + .stream() + .filter(Objects::nonNull) + .toList(); + register(endpointResources, config); + } + + private void register(Collection resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); + } + + } + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/package-info.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/package-info.java new file mode 100644 index 00000000000..98777e6e572 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present 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 web endpoint support. + */ +@NullMarked +package org.springframework.boot.jersey.autoconfigure.actuate.endpoint.web; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyChildManagementContextConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyChildManagementContextConfiguration.java new file mode 100644 index 00000000000..5ced2d4faf1 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyChildManagementContextConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.jersey.autoconfigure.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 4.0.0 + */ +@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false) +@Import(JerseyManagementContextConfiguration.class) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass({ JerseyApplicationPath.class, 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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyEndpointManagementContextConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyEndpointManagementContextConfiguration.java new file mode 100644 index 00000000000..451e2f83b63 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyEndpointManagementContextConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.endpoint.EndpointAccessResolver; +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.jersey.autoconfigure.JerseyApplicationPath; +import org.springframework.context.annotation.Bean; + +/** + * {@link ManagementContextConfiguration @ManagementContextConfiguration} for Jetty + * endpoints. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @since 4.0.0 + */ +@ManagementContextConfiguration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") +public class JerseyEndpointManagementContextConfiguration { + + @Bean + @SuppressWarnings("removal") + public org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar servletEndpointRegistrar( + WebEndpointProperties properties, + org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier servletEndpointsSupplier, + JerseyApplicationPath jerseyApplicationPath, EndpointAccessResolver endpointAccessResolver) { + return new org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar( + jerseyApplicationPath.getRelativePath(properties.getBasePath()), + servletEndpointsSupplier.getEndpoints(), endpointAccessResolver); + } + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyManagementContextConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyManagementContextConfiguration.java new file mode 100644 index 00000000000..0567ed07dfa --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyManagementContextConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; + +import org.springframework.boot.jersey.autoconfigure.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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseySameManagementContextConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseySameManagementContextConfiguration.java new file mode 100644 index 00000000000..d38ea132252 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseySameManagementContextConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jersey.autoconfigure.DefaultJerseyApplicationPath; +import org.springframework.boot.jersey.autoconfigure.JerseyApplicationPath; +import org.springframework.boot.jersey.autoconfigure.JerseyProperties; +import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; +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 4.0.0 + */ +@ManagementContextConfiguration(value = ManagementContextType.SAME, proxyBeanMethods = false) +@EnableConfigurationProperties(JerseyProperties.class) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass({ JerseyApplicationPath.class, 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 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/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointManagementContextConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointManagementContextConfiguration.java new file mode 100644 index 00000000000..ca2e26d654f --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointManagementContextConfiguration.java @@ -0,0 +1,242 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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 com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Priority; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.ext.ContextResolver; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.jspecify.annotations.Nullable; + +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.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.OperationResponseBody; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper; +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.WebEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; +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.boot.health.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; +import org.springframework.boot.jersey.actuate.endpoint.web.JerseyEndpointResourceFactory; +import org.springframework.boot.jersey.actuate.endpoint.web.JerseyHealthEndpointAdditionalPathResourceFactory; +import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; +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 + @SuppressWarnings("removal") + JerseyWebEndpointsResourcesRegistrar jerseyWebEndpointsResourcesRegistrar(Environment environment, + WebEndpointsSupplier webEndpointsSupplier, + org.springframework.boot.actuate.endpoint.web.annotation.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 healthEndpoint = webEndpoints.stream() + .filter((endpoint) -> endpoint.getEndpointId().equals(HEALTH_ENDPOINT_ID)) + .findFirst() + .orElse(null); + return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(healthEndpoint, + healthEndpointGroups); + } + + @Bean + @ConditionalOnBean(EndpointJackson2ObjectMapper.class) + @SuppressWarnings("removal") + ResourceConfigCustomizer endpointJackson2ObjectMapperResourceConfigCustomizer( + org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper endpointJackson2ObjectMapper) { + return (config) -> config.register( + new EndpointJackson2ObjectMapperContextResolver(endpointJackson2ObjectMapper), ContextResolver.class); + } + + 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. + */ + @SuppressWarnings("removal") + static class JerseyWebEndpointsResourcesRegistrar implements ManagementContextResourceConfigCustomizer { + + private final WebEndpointsSupplier webEndpointsSupplier; + + private final org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier servletEndpointsSupplier; + + private final EndpointMediaTypes mediaTypes; + + private final String basePath; + + private final boolean shouldRegisterLinks; + + JerseyWebEndpointsResourcesRegistrar(WebEndpointsSupplier webEndpointsSupplier, + org.springframework.boot.actuate.endpoint.web.annotation.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 @Nullable ExposableWebEndpoint healthEndpoint; + + private final HealthEndpointGroups groups; + + JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(@Nullable ExposableWebEndpoint healthEndpoint, + HealthEndpointGroups groups) { + this.healthEndpoint = healthEndpoint; + this.groups = groups; + } + + @Override + public void customize(ResourceConfig config) { + if (this.healthEndpoint != null) { + register(config, this.healthEndpoint); + } + } + + private void register(ResourceConfig config, ExposableWebEndpoint healthEndpoint) { + EndpointMapping mapping = new EndpointMapping(""); + JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( + WebServerNamespace.MANAGEMENT, this.groups); + Collection endpointResources = resourceFactory + .createEndpointResources(mapping, Collections.singletonList(healthEndpoint)) + .stream() + .filter(Objects::nonNull) + .toList(); + register(endpointResources, config); + } + + private void register(Collection resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); + } + + } + + /** + * {@link ContextResolver} used to obtain the {@link ObjectMapper} that should be used + * for {@link OperationResponseBody} instances. + */ + @SuppressWarnings("removal") + @Priority(Priorities.USER - 100) + private static final class EndpointJackson2ObjectMapperContextResolver implements ContextResolver { + + private final org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper mapper; + + private EndpointJackson2ObjectMapperContextResolver( + org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public @Nullable ObjectMapper getContext(Class type) { + return OperationResponseBody.class.isAssignableFrom(type) ? this.mapper.get() : null; + } + + } + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/ManagementContextResourceConfigCustomizer.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/ManagementContextResourceConfigCustomizer.java new file mode 100644 index 00000000000..f08e9c8d33b --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/ManagementContextResourceConfigCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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 4.0.0 + */ +public interface ManagementContextResourceConfigCustomizer { + + /** + * Customize the resource config. + * @param config the {@link ResourceConfig} to customize + */ + void customize(ResourceConfig config); + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/package-info.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/package-info.java new file mode 100644 index 00000000000..0519b3730b1 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/actuate/web/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present 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 web concerns. + */ +@NullMarked +package org.springframework.boot.jersey.autoconfigure.actuate.web; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/metrics/JerseyServerMetricsAutoConfiguration.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/metrics/JerseyServerMetricsAutoConfiguration.java new file mode 100644 index 00000000000..480d3ab5905 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/metrics/JerseyServerMetricsAutoConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.metrics; + +import io.micrometer.observation.ObservationRegistry; +import org.glassfish.jersey.micrometer.server.JerseyObservationConvention; +import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.beans.factory.ObjectProvider; +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.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; +import org.springframework.boot.micrometer.metrics.MaximumAllowableTagsMeterFilter; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsProperties; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Jersey server instrumentation. + * + * @author Michael Weirauch + * @author Michael Simons + * @author Andy Wilkinson + * @author Moritz Halbritter + * @since 4.0.0 + */ +@AutoConfiguration( + afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass({ ResourceConfig.class, ObservationApplicationEventListener.class }) +@ConditionalOnBean({ ResourceConfig.class, ObservationRegistry.class }) +@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) +public final class JerseyServerMetricsAutoConfiguration { + + private final ObservationProperties observationProperties; + + JerseyServerMetricsAutoConfiguration(ObservationProperties observationProperties) { + this.observationProperties = observationProperties; + } + + @Bean + ResourceConfigCustomizer jerseyServerObservationResourceConfigCustomizer(ObservationRegistry observationRegistry, + ObjectProvider jerseyObservationConvention) { + String metricName = this.observationProperties.getHttp().getServer().getRequests().getName(); + return (config) -> config.register(new ObservationApplicationEventListener(observationRegistry, metricName, + jerseyObservationConvention.getIfAvailable())); + } + + @Bean + @Order(0) + MaximumAllowableTagsMeterFilter jerseyMetricsUriTagFilter(MetricsProperties metricsProperties) { + String meterNamePrefix = this.observationProperties.getHttp().getServer().getRequests().getName(); + int maxUriTags = metricsProperties.getWeb().getServer().getMaxUriTags(); + return new MaximumAllowableTagsMeterFilter(meterNamePrefix, "uri", maxUriTags); + } + +} diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/metrics/package-info.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/metrics/package-info.java new file mode 100644 index 00000000000..c7d6b9a2d5f --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/metrics/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present 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 metrics. + */ +@NullMarked +package org.springframework.boot.jersey.autoconfigure.metrics; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/package-info.java b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/package-info.java new file mode 100644 index 00000000000..69ba84813d3 --- /dev/null +++ b/module/spring-boot-jersey/src/main/java/org/springframework/boot/jersey/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present 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. + */ +@NullMarked +package org.springframework.boot.jersey.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-jersey/src/main/resources/META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports b/module/spring-boot-jersey/src/main/resources/META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports new file mode 100644 index 00000000000..fcb89fe94d6 --- /dev/null +++ b/module/spring-boot-jersey/src/main/resources/META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports @@ -0,0 +1,4 @@ +org.springframework.boot.jersey.autoconfigure.actuate.web.JerseyChildManagementContextConfiguration +org.springframework.boot.jersey.autoconfigure.actuate.web.JerseyEndpointManagementContextConfiguration +org.springframework.boot.jersey.autoconfigure.actuate.web.JerseySameManagementContextConfiguration +org.springframework.boot.jersey.autoconfigure.actuate.web.JerseyWebEndpointManagementContextConfiguration diff --git a/module/spring-boot-jersey/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-jersey/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..2c4bbf79ba8 --- /dev/null +++ b/module/spring-boot-jersey/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration +org.springframework.boot.jersey.autoconfigure.actuate.endpoint.web.HealthEndpointJerseyExtensionAutoConfiguration +org.springframework.boot.jersey.autoconfigure.metrics.JerseyServerMetricsAutoConfiguration diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyApplicationPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyApplicationPathTests.java new file mode 100644 index 00000000000..b2678e6ad03 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyApplicationPathTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomApplicationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomApplicationTests.java new file mode 100644 index 00000000000..0056e079e4f --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomApplicationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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) +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterContextPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterContextPathTests.java new file mode 100644 index 00000000000..980fbb7a681 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterContextPathTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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" }) +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterPathTests.java new file mode 100644 index 00000000000..7c80d224b03 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomFilterPathTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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" }) +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomLoadOnStartupTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomLoadOnStartupTests.java new file mode 100644 index 00000000000..70aed79112c --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomLoadOnStartupTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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({ JerseyAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomObjectMapperProviderTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomObjectMapperProviderTests.java new file mode 100644 index 00000000000..d60816f68aa --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomObjectMapperProviderTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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.jackson2.default-property-inclusion=non_null") +@AutoConfigureTestRestTemplate +@DirtiesContext +class JerseyAutoConfigurationCustomObjectMapperProviderTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void contextLoads() { + ResponseEntity response = this.restTemplate.getForEntity("/rest/message", String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + 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; + } + + } + + @SuppressWarnings("removal") + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Configuration + @Import({ TomcatServletWebServerAutoConfiguration.class, + org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class, + JerseyAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletContextPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletContextPathTests.java new file mode 100644 index 00000000000..7bd41efe1b1 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletContextPathTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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") +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletPathTests.java new file mode 100644 index 00000000000..e402cbbc644 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationCustomServletPathTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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) +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultFilterPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultFilterPathTests.java new file mode 100644 index 00000000000..6e4523a9cd9 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultFilterPathTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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" }) +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultServletPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultServletPathTests.java new file mode 100644 index 00000000000..bbbf7445551 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationDefaultServletPathTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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) +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationObjectMapperProviderTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationObjectMapperProviderTests.java new file mode 100644 index 00000000000..d6f5fa89281 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationObjectMapperProviderTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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.jackson2.default-property-inclusion:non-null") +@AutoConfigureTestRestTemplate +@DirtiesContext +class JerseyAutoConfigurationObjectMapperProviderTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void responseIsSerializedUsingAutoConfiguredObjectMapper() { + ResponseEntity response = this.restTemplate.getForEntity("/rest/message", String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + 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 + @SuppressWarnings("removal") + @Import({ TomcatServletWebServerAutoConfiguration.class, + org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class, + JerseyAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationServletContainerTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationServletContainerTests.java new file mode 100644 index 00000000000..5eeab6d9945 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationServletContainerTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.jersey.autoconfigure.JerseyAutoConfigurationServletContainerTests.Application; +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.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.boot.tomcat.servlet.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({ TomcatServletWebServerAutoConfiguration.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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationTests.java new file mode 100644 index 00000000000..871a55aacd7 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +import java.util.Collections; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration.JerseyWebApplicationInitializer; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockServletContext; +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 + @SuppressWarnings("removal") + void whenJaxbIsAvailableTheObjectMapperIsCustomizedWithAnAnnotationIntrospector() { + this.contextRunner + .withConfiguration(AutoConfigurations + .of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class)) + .run((context) -> { + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + assertThat(objectMapper.getSerializationConfig() + .getAnnotationIntrospector() + .allIntrospectors() + .stream() + .filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).hasSize(1); + }); + } + + @Test + @SuppressWarnings("removal") + void whenJaxbIsNotAvailableTheObjectMapperCustomizationBacksOff() { + this.contextRunner + .withConfiguration(AutoConfigurations + .of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.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 + @SuppressWarnings("removal") + void whenJacksonJaxbModuleIsNotAvailableTheObjectMapperCustomizationBacksOff() { + this.contextRunner + .withConfiguration(AutoConfigurations + .of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.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(); + }); + } + + @Test + void webApplicationInitializerDisablesJerseyWebApplicationInitializer() throws ServletException { + ServletContext context = new MockServletContext(); + new JerseyWebApplicationInitializer().onStartup(context); + assertThat(context.getInitParameter("contextConfigLocation")).isEqualTo(""); + } + + @Test + @ClassPathExclusions("jersey-spring6-*.jar") + void webApplicationInitializerHasNoEffectWhenJerseyIsAbsent() throws ServletException { + ServletContext context = new MockServletContext(); + new JerseyWebApplicationInitializer().onStartup(context); + assertThat(Collections.list(context.getInitParameterNames())).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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationWithoutApplicationPathTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationWithoutApplicationPathTests.java new file mode 100644 index 00000000000..51d36468c23 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/JerseyAutoConfigurationWithoutApplicationPathTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure; + +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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +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") +@AutoConfigureTestRestTemplate +@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({ TomcatServletWebServerAutoConfiguration.class, JerseyAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected @interface MinimalWebConfiguration { + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/HealthEndpointJerseyExtensionAutoConfigurationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/HealthEndpointJerseyExtensionAutoConfigurationTests.java new file mode 100644 index 00000000000..029654f1031 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/HealthEndpointJerseyExtensionAutoConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.endpoint.web; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.WithTestEndpointOutcomeExposureContributor; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.health.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.health.actuate.endpoint.HealthEndpointWebExtension; +import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointAutoConfiguration; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; +import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.health.contributor.ReactiveHealthIndicator; +import org.springframework.boot.jersey.autoconfigure.actuate.endpoint.web.HealthEndpointJerseyExtensionAutoConfiguration.JerseyAdditionalHealthEndpointPathsResourcesRegistrar; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HealthEndpointJerseyExtensionAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class HealthEndpointJerseyExtensionAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withUserConfiguration(HealthIndicatorsConfiguration.class) + .withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class, + HealthContributorRegistryAutoConfiguration.class, HealthEndpointAutoConfiguration.class, + HealthEndpointJerseyExtensionAutoConfiguration.class)); + + @Test + @WithTestEndpointOutcomeExposureContributor + void additionalJerseyHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.exclude=*", + "management.endpoints.test.exposure.include=*") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(HealthEndpoint.class); + assertThat(context).hasSingleBean(HealthEndpointWebExtension.class); + assertThat(context.getBean(WebEndpointsSupplier.class).getEndpoints()).isEmpty(); + assertThat(context).hasSingleBean(JerseyAdditionalHealthEndpointPathsResourcesRegistrar.class); + }); + } + + @Configuration(proxyBeanMethods = false) + static class HealthIndicatorsConfiguration { + + @Bean + HealthIndicator simpleHealthIndicator() { + return () -> Health.up().withDetail("counter", 42).build(); + } + + @Bean + HealthIndicator additionalHealthIndicator() { + return () -> Health.up().build(); + } + + @Bean + ReactiveHealthIndicator reactiveHealthIndicator() { + return () -> Mono.just(Health.up().build()); + } + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/JerseyEndpointAccessIntegrationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/JerseyEndpointAccessIntegrationTests.java new file mode 100644 index 00000000000..ce57643d43f --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/JerseyEndpointAccessIntegrationTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.endpoint.web; + +import java.io.IOException; +import java.time.Duration; +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.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.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.client.ExchangeStrategies; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for controlling access to endpoints exposed by Jersey. + * + * @author Andy Wilkinson + */ +class JerseyEndpointAccessIntegrationTests { + + @SuppressWarnings("removal") + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration( + AutoConfigurations.of(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class, + JerseyAutoConfiguration.class, EndpointAutoConfiguration.class, + TomcatServletWebServerAutoConfiguration.class, WebEndpointAutoConfiguration.class, + ManagementContextAutoConfiguration.class, BeansEndpointAutoConfiguration.class)) + .withUserConfiguration(CustomServletEndpoint.class) + .withPropertyValues("server.port:0"); + + @Test + void accessIsUnrestrictedByDefault() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*").run((context) -> { + WebTestClient client = createClient(context); + assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue(); + assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue(); + assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isTrue(); + }); + } + + @Test + void accessCanBeReadOnlyByDefault() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=*", + "management.endpoints.access.default=READ_ONLY") + .run((context) -> { + WebTestClient client = createClient(context); + assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue(); + assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue(); + assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse(); + }); + } + + @Test + void accessCanBeNoneByDefault() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=*", + "management.endpoints.access.default=NONE") + .run((context) -> { + WebTestClient client = createClient(context); + assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse(); + assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isFalse(); + assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse(); + }); + } + + @Test + void accessForOneEndpointCanOverrideTheDefaultAccess() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=*", + "management.endpoints.access.default=NONE", "management.endpoint.customservlet.access=READ_ONLY") + .run((context) -> { + WebTestClient client = createClient(context); + assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse(); + assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue(); + assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse(); + }); + } + + @Test + void accessCanBeCappedAtReadOnly() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=*", + "management.endpoints.access.default=UNRESTRICTED", + "management.endpoints.access.max-permitted=READ_ONLY") + .run((context) -> { + WebTestClient client = createClient(context); + assertThat(isAccessible(client, HttpMethod.GET, "beans")).isTrue(); + assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isTrue(); + assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse(); + }); + } + + @Test + void accessCanBeCappedAtNone() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=*", + "management.endpoints.access.default=UNRESTRICTED", "management.endpoints.access.max-permitted=NONE") + .run((context) -> { + WebTestClient client = createClient(context); + assertThat(isAccessible(client, HttpMethod.GET, "beans")).isFalse(); + assertThat(isAccessible(client, HttpMethod.GET, "customservlet")).isFalse(); + assertThat(isAccessible(client, HttpMethod.POST, "customservlet")).isFalse(); + }); + } + + private WebTestClient createClient(AssertableWebApplicationContext context) { + int port = context.getSourceApplicationContext(ServletWebServerApplicationContext.class) + .getWebServer() + .getPort(); + ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() + .codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(-1)) + .build(); + return WebTestClient.bindToServer() + .baseUrl("http://localhost:" + port) + .exchangeStrategies(exchangeStrategies) + .responseTimeout(Duration.ofMinutes(5)) + .build(); + } + + private boolean isAccessible(WebTestClient client, HttpMethod method, String path) { + path = "/actuator/" + path; + EntityExchangeResult result = client.method(method).uri(path).exchange().expectBody().returnResult(); + if (result.getStatus() == HttpStatus.OK) { + return true; + } + if (result.getStatus() == HttpStatus.NOT_FOUND || result.getStatus() == HttpStatus.METHOD_NOT_ALLOWED) { + return false; + } + throw new IllegalStateException( + String.format("Unexpected %s HTTP status for endpoint %s", result.getStatus(), path)); + } + + @org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint(id = "customservlet") + @SuppressWarnings({ "deprecation", "removal" }) + static class CustomServletEndpoint + implements Supplier { + + @Override + public org.springframework.boot.actuate.endpoint.web.EndpointServlet get() { + return new org.springframework.boot.actuate.endpoint.web.EndpointServlet(new HttpServlet() { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + } + + }); + } + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/JerseyEndpointIntegrationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/JerseyEndpointIntegrationTests.java new file mode 100644 index 00000000000..cf47a77338d --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/endpoint/web/JerseyEndpointIntegrationTests.java @@ -0,0 +1,227 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.endpoint.web; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +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.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 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 endpointObjectMapperCanBeApplied() { + WebApplicationContextRunner contextRunner = getContextRunner(new Class[] { EndpointsConfiguration.class, + ResourceConfigConfiguration.class, EndpointObjectMapperConfiguration.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/beans").exchange().expectStatus().isOk().expectBody().consumeWith((result) -> { + String json = new String(result.getResponseBody(), StandardCharsets.UTF_8); + assertThat(json).contains("\"scope\":\"notelgnis\""); + }); + }); + } + + 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) { + return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(getAutoconfigurations(additionalAutoConfigurations))) + .withUserConfiguration(userConfigurations) + .withPropertyValues("management.endpoints.web.exposure.include:*", "server.port:0"); + } + + @SuppressWarnings("removal") + private Class[] getAutoconfigurations(Class... additional) { + List> autoconfigurations = new ArrayList<>( + Arrays.asList(org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class, + JerseyAutoConfiguration.class, EndpointAutoConfiguration.class, + TomcatServletWebServerAutoConfiguration.class, WebEndpointAutoConfiguration.class, + ManagementContextAutoConfiguration.class, BeansEndpointAutoConfiguration.class)); + autoconfigurations.addAll(Arrays.asList(additional)); + return autoconfigurations.toArray(new Class[0]); + } + + @org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint(id = "controller") + @SuppressWarnings("removal") + static class TestControllerEndpoint { + + } + + @org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint(id = "restcontroller") + @SuppressWarnings("removal") + 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(); + } + + } + + @Configuration + @SuppressWarnings({ "deprecation", "removal" }) + static class EndpointObjectMapperConfiguration { + + @Bean + org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper endpointJackson2ObjectMapper() { + SimpleModule module = new SimpleModule(); + module.addSerializer(String.class, new ReverseStringSerializer()); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(module); + return () -> objectMapper; + } + + static class ReverseStringSerializer extends StdScalarSerializer { + + ReverseStringSerializer() { + super(String.class, false); + } + + @Override + public boolean isEmpty(SerializerProvider provider, Object value) { + return ((String) value).isEmpty(); + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { + serialize(value, gen); + } + + @Override + public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, + TypeSerializer typeSer) throws IOException { + serialize(value, gen); + } + + private void serialize(Object value, JsonGenerator gen) throws IOException { + StringBuilder builder = new StringBuilder((String) value); + gen.writeString(builder.reverse().toString()); + } + + } + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyChildManagementContextConfigurationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyChildManagementContextConfigurationTests.java new file mode 100644 index 00000000000..c9722f9d1e1 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyChildManagementContextConfigurationTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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.jersey.autoconfigure.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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyEndpointManagementContextConfigurationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyEndpointManagementContextConfigurationTests.java new file mode 100644 index 00000000000..a7c3cd17a83 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyEndpointManagementContextConfigurationTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.endpoint.Access; +import org.springframework.boot.actuate.endpoint.EndpointAccessResolver; +import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jersey.autoconfigure.JerseyApplicationPath; +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 static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JerseyEndpointManagementContextConfiguration}. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("removal") +class JerseyEndpointManagementContextConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withUserConfiguration(TestConfig.class); + + @Test + void contextShouldContainServletEndpointRegistrar() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ServletEndpointRegistrar.class); + ServletEndpointRegistrar bean = context.getBean(ServletEndpointRegistrar.class); + assertThat(bean).hasFieldOrPropertyWithValue("basePath", "/test/actuator"); + }); + } + + @Test + void contextWhenNotServletBasedShouldNotContainServletEndpointRegistrar() { + new ApplicationContextRunner().withUserConfiguration(TestConfig.class) + .run((context) -> assertThat(context).doesNotHaveBean(ServletEndpointRegistrar.class)); + } + + @Configuration(proxyBeanMethods = false) + @Import(JerseyEndpointManagementContextConfiguration.class) + @EnableConfigurationProperties(WebEndpointProperties.class) + static class TestConfig { + + @Bean + ServletEndpointsSupplier servletEndpointsSupplier() { + return Collections::emptyList; + } + + @Bean + JerseyApplicationPath jerseyApplicationPath() { + return () -> "/test"; + } + + @Bean + EndpointAccessResolver endpointAccessResolver() { + return (endpointId, defaultAccess) -> Access.UNRESTRICTED; + } + + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyHealthEndpointAdditionalPathIntegrationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyHealthEndpointAdditionalPathIntegrationTests.java new file mode 100644 index 00000000000..98fdbeb7f54 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyHealthEndpointAdditionalPathIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.integrationtest.AbstractHealthEndpointAdditionalPathIntegrationTests; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointAutoConfiguration; +import org.springframework.boot.health.autoconfigure.application.DiskSpaceHealthContributorAutoConfiguration; +import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration; +import org.springframework.boot.jersey.autoconfigure.actuate.endpoint.web.HealthEndpointJerseyExtensionAutoConfiguration; +import org.springframework.boot.servlet.autoconfigure.actuate.web.ServletManagementContextAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.tomcat.autoconfigure.actuate.web.server.TomcatServletManagementContextAutoConfiguration; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer; +import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.web.context.ConfigurableWebApplicationContext; + +/** + * Integration tests for health groups on an additional path on Jersey. + * + * @author Madhura Bhave + */ +class JerseyHealthEndpointAdditionalPathIntegrationTests extends + AbstractHealthEndpointAdditionalPathIntegrationTests { + + @SuppressWarnings("removal") + JerseyHealthEndpointAdditionalPathIntegrationTests() { + super(new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of( + org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class, + JerseyAutoConfiguration.class, EndpointAutoConfiguration.class, + TomcatServletWebServerAutoConfiguration.class, + TomcatServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class, + JerseyAutoConfiguration.class, ManagementContextAutoConfiguration.class, + ServletManagementContextAutoConfiguration.class, HealthEndpointAutoConfiguration.class, + HealthContributorRegistryAutoConfiguration.class, + HealthEndpointJerseyExtensionAutoConfiguration.class, + DiskSpaceHealthContributorAutoConfiguration.class)) + .withInitializer(new ServerPortInfoApplicationContextInitializer()) + .withPropertyValues("server.port=0")); + } + +} diff --git a/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseySameManagementContextConfigurationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseySameManagementContextConfigurationTests.java new file mode 100644 index 00000000000..88be8c9858b --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseySameManagementContextConfigurationTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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.jersey.autoconfigure.DefaultJerseyApplicationPath; +import org.springframework.boot.jersey.autoconfigure.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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointIntegrationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointIntegrationTests.java new file mode 100644 index 00000000000..225ab84227a --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointIntegrationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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.autoconfigure.AutoConfigurations; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +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, + TomcatServletWebServerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, JerseyWebEndpointManagementContextConfiguration.class)) + .withUserConfiguration(ResourceConfigConfiguration.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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointManagementContextConfigurationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointManagementContextConfigurationTests.java new file mode 100644 index 00000000000..972dfadb32f --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/actuate/web/JerseyWebEndpointManagementContextConfigurationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.actuate.web; + +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.endpoint.Access; +import org.springframework.boot.actuate.endpoint.EndpointAccessResolver; +import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jersey.autoconfigure.actuate.web.JerseyWebEndpointManagementContextConfiguration.JerseyWebEndpointsResourcesRegistrar; +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) + .withBean(EndpointAccessResolver.class, () -> (endpointId, defaultAccess) -> Access.UNRESTRICTED); + + @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/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/metrics/JerseyServerMetricsAutoConfigurationTests.java b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/metrics/JerseyServerMetricsAutoConfigurationTests.java new file mode 100644 index 00000000000..d45fe6b88b7 --- /dev/null +++ b/module/spring-boot-jersey/src/test/java/org/springframework/boot/jersey/autoconfigure/metrics/JerseyServerMetricsAutoConfigurationTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-present 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.jersey.autoconfigure.metrics; + +import java.net.URI; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.observation.MeterObservationHandler; +import io.micrometer.observation.Observation.Context; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration; +import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration; +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.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; +import org.springframework.boot.web.server.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 + * @author Moritz Halbritter + */ +class JerseyServerMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JerseyServerMetricsAutoConfiguration.class)); + + private final WebApplicationContextRunner webContextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class, + JerseyServerMetricsAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class, + TomcatServletWebServerAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, + ObservationAutoConfiguration.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).hasBean("jerseyMetricsUriTagFilter") + .hasSingleBean(ResourceConfigCustomizer.class)); + } + + @Test + void httpRequestsAreTimed() { + this.webContextRunner.withUserConfiguration(MetricsConfiguration.class).run((context) -> { + doRequest(context); + Thread.sleep(500); + MeterRegistry registry = context.getBean(MeterRegistry.class); + Timer timer = registry.get("http.server.requests").tag("uri", "/users/{id}").timer(); + assertThat(timer.count()).isOne(); + }); + } + + @Test + void noHttpRequestsTimedWhenJerseyInstrumentationMissingFromClasspath() { + this.webContextRunner.withClassLoader(new FilteredClassLoader(ObservationApplicationEventListener.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 static class TestResource { + + @GET + @Path("/{id}") + public String getUser(@PathParam("id") String id) { + return id; + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class MetricsConfiguration { + + @Bean + MeterObservationHandler meterObservationHandler(MeterRegistry registry) { + return new DefaultMeterObservationHandler(registry); + } + + } + +} diff --git a/module/spring-boot-jersey/src/testFixtures/java/org/springframework/boot/jersey/actuate/endpoint/web/test/JerseyEndpointConfiguration.java b/module/spring-boot-jersey/src/testFixtures/java/org/springframework/boot/jersey/actuate/endpoint/web/test/JerseyEndpointConfiguration.java new file mode 100644 index 00000000000..7426e9b9eb4 --- /dev/null +++ b/module/spring-boot-jersey/src/testFixtures/java/org/springframework/boot/jersey/actuate/endpoint/web/test/JerseyEndpointConfiguration.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-present 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.jersey.actuate.endpoint.web.test; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; + +import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +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.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.jersey.actuate.endpoint.web.JerseyEndpointResourceFactory; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration; +import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; +import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Endpoint configuration for Jersey. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + */ +@SuppressWarnings("removal") +@Configuration(proxyBeanMethods = false) +@ImportAutoConfiguration({ org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration.class, + JerseyAutoConfiguration.class }) +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(), 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)); + } + +} diff --git a/module/spring-boot-jersey/src/testFixtures/java/org/springframework/boot/jersey/actuate/endpoint/web/test/JerseyWebEndpointInfrastructureProvider.java b/module/spring-boot-jersey/src/testFixtures/java/org/springframework/boot/jersey/actuate/endpoint/web/test/JerseyWebEndpointInfrastructureProvider.java new file mode 100644 index 00000000000..8458d64032c --- /dev/null +++ b/module/spring-boot-jersey/src/testFixtures/java/org/springframework/boot/jersey/actuate/endpoint/web/test/JerseyWebEndpointInfrastructureProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present 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.jersey.actuate.endpoint.web.test; + +import java.util.List; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointInfrastructureProvider; +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest.Infrastructure; + +/** + * {@link WebEndpointInfrastructureProvider} for Jersey. + * + * @author Stephane Nicoll + */ +class JerseyWebEndpointInfrastructureProvider implements WebEndpointInfrastructureProvider { + + @Override + public boolean supports(Infrastructure infrastructure) { + return infrastructure == Infrastructure.JERSEY; + } + + @Override + public List> getInfrastructureConfiguration(Infrastructure infrastructure) { + return List.of(JerseyEndpointConfiguration.class); + } + +} diff --git a/module/spring-boot-jersey/src/testFixtures/resources/META-INF/spring.factories b/module/spring-boot-jersey/src/testFixtures/resources/META-INF/spring.factories new file mode 100644 index 00000000000..7163b1dad5d --- /dev/null +++ b/module/spring-boot-jersey/src/testFixtures/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.actuate.endpoint.web.test.WebEndpointInfrastructureProvider=\ +org.springframework.boot.jersey.actuate.endpoint.web.test.JerseyWebEndpointInfrastructureProvider diff --git a/module/spring-boot-quartz/build.gradle b/module/spring-boot-quartz/build.gradle index 1c77924ea60..73ba9b02c52 100644 --- a/module/spring-boot-quartz/build.gradle +++ b/module/spring-boot-quartz/build.gradle @@ -39,6 +39,7 @@ dependencies { testImplementation(project(":module:spring-boot-liquibase")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":module:spring-boot-sql"))) + testImplementation(testFixtures(project(":module:spring-boot-jersey"))) testImplementation(testFixtures(project(":module:spring-boot-webflux"))) testImplementation(testFixtures(project(":module:spring-boot-webmvc"))) testImplementation("net.minidev:json-smart") diff --git a/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/actuate/endpoint/QuartzEndpoint.java b/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/actuate/endpoint/QuartzEndpoint.java index 9a236fa2dc9..1153880053c 100644 --- a/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/actuate/endpoint/QuartzEndpoint.java +++ b/module/spring-boot-quartz/src/main/java/org/springframework/boot/quartz/actuate/endpoint/QuartzEndpoint.java @@ -415,7 +415,7 @@ public class QuartzEndpoint { /** * Description of a triggered on-demand {@link Job Quartz Job}. */ - public static final class QuartzJobTriggerDescriptor { + public static final class QuartzJobTriggerDescriptor implements OperationResponseBody { private final String group; diff --git a/module/spring-boot-security/build.gradle b/module/spring-boot-security/build.gradle index 1025954f2bf..f948f1e2735 100644 --- a/module/spring-boot-security/build.gradle +++ b/module/spring-boot-security/build.gradle @@ -34,6 +34,7 @@ dependencies { optional(project(":module:spring-boot-actuator-autoconfigure")) optional(project(":module:spring-boot-h2console")) optional(project(":module:spring-boot-health")) + optional(project(":module:spring-boot-jersey")) optional(project(":module:spring-boot-reactor")) optional(project(":module:spring-boot-restclient-test")) optional(project(":module:spring-boot-rsocket")) diff --git a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfiguration.java b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfiguration.java index 58317b798d3..f1d4f24c531 100644 --- a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfiguration.java +++ b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfiguration.java @@ -16,12 +16,16 @@ package org.springframework.boot.security.autoconfigure.actuate.web.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.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.jersey.autoconfigure.JerseyApplicationPath; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -53,4 +57,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 PathPatternRequestMatcherProvider(applicationPath::getRelativePath); + } + + } + } diff --git a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/JerseyEndpointRequestIntegrationTests.java b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/JerseyEndpointRequestIntegrationTests.java new file mode 100644 index 00000000000..b3bea546011 --- /dev/null +++ b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/JerseyEndpointRequestIntegrationTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-present 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.security.autoconfigure.actuate.web.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.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jersey.autoconfigure.JerseyAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.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/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java index 5b992ae37bf..6851b57f2e2 100644 --- a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java +++ b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.security.autoconfigure.actuate.web.servlet; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jersey.autoconfigure.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,19 @@ class SecurityRequestMatchersManagementContextConfigurationTests { }); } + @Test + void registersRequestMatcherForJerseyProviderIfJerseyPresentAndMvcAbsent() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) + .withUserConfiguration(TestJerseyConfiguration.class) + .run((context) -> { + PathPatternRequestMatcherProvider matcherProvider = context + .getBean(PathPatternRequestMatcherProvider.class); + RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example", null); + assertThat(requestMatcher).extracting("pattern") + .isEqualTo(PathPatternParser.defaultInstance.parse("/admin/example")); + }); + } + @Test void mvcRequestMatcherProviderConditionalOnDispatcherServletClass() { this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet")) @@ -103,4 +117,14 @@ class SecurityRequestMatchersManagementContextConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class TestJerseyConfiguration { + + @Bean + JerseyApplicationPath jerseyApplicationPath() { + return () -> "/admin"; + } + + } + } diff --git a/module/spring-boot-session/build.gradle b/module/spring-boot-session/build.gradle index 1ead82d474e..2827b513528 100644 --- a/module/spring-boot-session/build.gradle +++ b/module/spring-boot-session/build.gradle @@ -48,6 +48,7 @@ dependencies { testImplementation(project(":module:spring-boot-tomcat")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(project(":module:spring-boot-webflux")) + testImplementation(testFixtures(project(":module:spring-boot-jersey"))) testImplementation(testFixtures(project(":module:spring-boot-webflux"))) testImplementation(testFixtures(project(":module:spring-boot-webmvc"))) testImplementation(testFixtures(project(":module:spring-boot-web-server"))) diff --git a/module/spring-boot-session/src/test/java/org/springframework/boot/session/actuate/endpoint/SessionsEndpointWebIntegrationTests.java b/module/spring-boot-session/src/test/java/org/springframework/boot/session/actuate/endpoint/SessionsEndpointWebIntegrationTests.java index cf6957065f3..d846e3b713d 100644 --- a/module/spring-boot-session/src/test/java/org/springframework/boot/session/actuate/endpoint/SessionsEndpointWebIntegrationTests.java +++ b/module/spring-boot-session/src/test/java/org/springframework/boot/session/actuate/endpoint/SessionsEndpointWebIntegrationTests.java @@ -33,7 +33,8 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Integration tests for {@link SessionsEndpoint} exposed by Spring MVC and WebFlux. + * Integration tests for {@link SessionsEndpoint} exposed by Jersey, Spring MVC, and + * WebFlux. * * @author Vedran Pavic */ @@ -45,7 +46,7 @@ class SessionsEndpointWebIntegrationTests { private static final FindByIndexNameSessionRepository repository = mock( FindByIndexNameSessionRepository.class); - @WebEndpointTest(infrastructure = Infrastructure.MVC) + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionsForUsernameWithoutUsernameParam(WebTestClient client) { client.get() .uri((builder) -> builder.path("/actuator/sessions").build()) @@ -54,7 +55,7 @@ class SessionsEndpointWebIntegrationTests { .isBadRequest(); } - @WebEndpointTest(infrastructure = Infrastructure.MVC) + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionsForUsernameNoResults(WebTestClient client) { given(repository.findByPrincipalName("user")).willReturn(Collections.emptyMap()); client.get() @@ -67,7 +68,7 @@ class SessionsEndpointWebIntegrationTests { .isEmpty(); } - @WebEndpointTest(infrastructure = Infrastructure.MVC) + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionsForUsernameFound(WebTestClient client) { given(repository.findByPrincipalName("user")).willReturn(Collections.singletonMap(session.getId(), session)); client.get() @@ -80,7 +81,7 @@ class SessionsEndpointWebIntegrationTests { .isEqualTo(new JSONArray().appendElement(session.getId())); } - @WebEndpointTest(infrastructure = Infrastructure.MVC) + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionForIdNotFound(WebTestClient client) { client.get() .uri((builder) -> builder.path("/actuator/sessions/session-id-not-found").build()) @@ -89,7 +90,7 @@ class SessionsEndpointWebIntegrationTests { .isNotFound(); } - @WebEndpointTest(infrastructure = Infrastructure.MVC) + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void deleteSession(WebTestClient client) { client.delete() .uri((builder) -> builder.path("/actuator/sessions/{id}").build(session.getId())) diff --git a/module/spring-boot-webmvc/build.gradle b/module/spring-boot-webmvc/build.gradle index ce06ce2caca..d33b6acafa2 100644 --- a/module/spring-boot-webmvc/build.gradle +++ b/module/spring-boot-webmvc/build.gradle @@ -49,7 +49,6 @@ dependencies { testFixturesApi(testFixtures(project(":module:spring-boot-actuator"))) testFixturesImplementation(project(":module:spring-boot-http-converter")) testFixturesImplementation(project(":module:spring-boot-jackson")) - testFixturesImplementation(project(":module:spring-boot-tomcat")) testImplementation(project(":core:spring-boot-test")) testImplementation(project(":module:spring-boot-jackson")) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index f25471613e9..ac2fb1105dd 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1017,6 +1017,16 @@ bom { releaseNotes("https://github.com/redis/jedis/releases/tag/v{version}") } } + library("Jersey", "4.0.0") { + group("org.glassfish.jersey") { + bom("jersey-bom") + } + links { + site("https://github.com/eclipse-ee4j/jersey") + javadoc("https://javadoc.io/doc/org.glassfish.jersey.core/jersey-server/{version}", "org.glassfish.jersey.server") + releaseNotes("https://github.com/eclipse-ee4j/jersey/releases/tag/{version}") + } + } library("Jetty Reactive HTTPClient", "4.1.0") { group("org.eclipse.jetty") { modules = [ @@ -2044,6 +2054,7 @@ bom { "spring-boot-jarmode-tools", "spring-boot-jdbc", "spring-boot-jdbc-test", + "spring-boot-jersey", "spring-boot-jetty", "spring-boot-jetty-runtime", "spring-boot-jms", @@ -2171,6 +2182,8 @@ bom { "spring-boot-starter-jackson-test", "spring-boot-starter-jdbc", "spring-boot-starter-jdbc-test", + "spring-boot-starter-jersey", + "spring-boot-starter-jersey-test", "spring-boot-starter-jetty", "spring-boot-starter-jms", "spring-boot-starter-jms-test", diff --git a/settings.gradle b/settings.gradle index 3204d1c1059..5ee4a54aa30 100644 --- a/settings.gradle +++ b/settings.gradle @@ -135,6 +135,7 @@ include "module:spring-boot-jackson" include "module:spring-boot-jackson2" include "module:spring-boot-jdbc" include "module:spring-boot-jdbc-test" +include "module:spring-boot-jersey" include "module:spring-boot-jetty" include "module:spring-boot-jetty-runtime" include "module:spring-boot-jms" @@ -279,6 +280,8 @@ include "starter:spring-boot-starter-jackson" include "starter:spring-boot-starter-jackson-test" include "starter:spring-boot-starter-jdbc" include "starter:spring-boot-starter-jdbc-test" +include "starter:spring-boot-starter-jersey" +include "starter:spring-boot-starter-jersey-test" include "starter:spring-boot-starter-jetty" include "starter:spring-boot-starter-jms" include "starter:spring-boot-starter-jms-test" @@ -414,6 +417,7 @@ include ":smoke-test:spring-boot-smoke-test-hibernate" include ":smoke-test:spring-boot-smoke-test-integration" include ":smoke-test:spring-boot-smoke-test-jackson2-mixed" include ":smoke-test:spring-boot-smoke-test-jackson2-only" +include ":smoke-test:spring-boot-smoke-test-jersey" include ":smoke-test:spring-boot-smoke-test-jetty" include ":smoke-test:spring-boot-smoke-test-jetty-jsp" include ":smoke-test:spring-boot-smoke-test-jetty-ssl" @@ -434,6 +438,7 @@ include ":smoke-test:spring-boot-smoke-test-reactive-oauth2-resource-server" include ":smoke-test:spring-boot-smoke-test-rsocket" include ":smoke-test:spring-boot-smoke-test-saml2-service-provider" include ":smoke-test:spring-boot-smoke-test-secure" +include ":smoke-test:spring-boot-smoke-test-secure-jersey" include ":smoke-test:spring-boot-smoke-test-secure-webflux" include ":smoke-test:spring-boot-smoke-test-servlet" include ":smoke-test:spring-boot-smoke-test-session-jdbc" diff --git a/smoke-test/spring-boot-smoke-test-jersey/build.gradle b/smoke-test/spring-boot-smoke-test-jersey/build.gradle new file mode 100644 index 00000000000..a7b77393744 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/build.gradle @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id "java" +} + +description = "Spring Boot Jersey smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-actuator")) + implementation(project(":starter:spring-boot-starter-jersey")) + implementation(project(":starter:spring-boot-starter-tomcat")) + + runtimeOnly("jakarta.xml.bind:jakarta.xml.bind-api") + + testImplementation(project(":starter:spring-boot-starter-jersey-test")) +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Endpoint.java b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Endpoint.java new file mode 100644 index 00000000000..880c5d27275 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Endpoint.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/JerseyConfig.java b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/JerseyConfig.java new file mode 100644 index 00000000000..2de4b450978 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/JerseyConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/ReverseEndpoint.java b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/ReverseEndpoint.java new file mode 100644 index 00000000000..9510f10a2a8 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/ReverseEndpoint.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java new file mode 100644 index 00000000000..13951906a54 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/SampleJerseyApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-present 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.context.metrics.buffering.BufferingApplicationStartup; +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) + .applicationStartup(new BufferingApplicationStartup(2048))) + .run(args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Service.java b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Service.java new file mode 100644 index 00000000000..52255bb08b3 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/Service.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present 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}") + @SuppressWarnings("NullAway.Init") + private String msg; + + public String message() { + return this.msg; + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/package-info.java b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/package-info.java new file mode 100644 index 00000000000..a2f1e3c5fc1 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/main/java/smoketest/jersey/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present 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. + */ + +@NullMarked +package smoketest.jersey; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/main/resources/application.properties b/smoke-test/spring-boot-smoke-test-jersey/src/main/resources/application.properties new file mode 100644 index 00000000000..037f2782e08 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/main/resources/application.properties @@ -0,0 +1,3 @@ +management.endpoints.web.exposure.include=* +management.endpoints.jackson.isolated-object-mapper=true +spring.jackson2.visibility.field=any diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java new file mode 100644 index 00000000000..a881a351f58 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyApplicationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-present 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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +@AutoConfigureTestRestTemplate +@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\",\"groups\":[\"liveness\",\"readiness\"]}"); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java new file mode 100644 index 00000000000..9b131217bff --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/AbstractJerseyManagementPortTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-present 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.junit.jupiter.api.Test; +import smoketest.jersey.AbstractJerseyManagementPortTests.ResourceConfigConfiguration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jersey.autoconfigure.ResourceConfigCustomizer; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +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 + */ +@AutoConfigureTestRestTemplate +@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 (config) -> config.register(TestEndpoint.class); + } + + @Path("/test") + public static class TestEndpoint { + + @GET + public String test() { + return "test"; + } + + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/ApplicationStartupSpringBootContextLoader.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/ApplicationStartupSpringBootContextLoader.java new file mode 100644 index 00000000000..bd5b931cf5b --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/ApplicationStartupSpringBootContextLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present 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.SpringApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.boot.test.context.SpringBootContextLoader; + +class ApplicationStartupSpringBootContextLoader extends SpringBootContextLoader { + + @Override + protected SpringApplication getSpringApplication() { + SpringApplication application = new SpringApplication(); + application.setApplicationStartup(new BufferingApplicationStartup(1024)); + return application; + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java new file mode 100644 index 00000000000..e86f85dd38b --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperFalseTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-present 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 com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +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 org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for Jersey actuator when not using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@AutoConfigureTestRestTemplate +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=false") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +class JerseyActuatorIsolatedObjectMapperFalseTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void resourceShouldBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class); + System.out.println(entity.getBody()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(entity.getBody()) + .contains("Java 8 date/time type `java.time.Clock$SystemClock` not supported by default"); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java new file mode 100644 index 00000000000..ae8acd1d89e --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyActuatorIsolatedObjectMapperTrueTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present 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 com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +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 org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for Jersey actuator when using an isolated {@link ObjectMapper}. + * + * @author Phillip Webb + */ +@AutoConfigureTestRestTemplate +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "management.endpoints.jackson.isolated-object-mapper=true") +@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class) +class JerseyActuatorIsolatedObjectMapperTrueTests { + + @LocalServerPort + private int port; + + @LocalManagementPort + private int managementPort; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void resourceShouldBeAvailableOnMainPort() { + ResponseEntity entity = this.testRestTemplate + .getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"timeline\":"); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java new file mode 100644 index 00000000000..c56376ec69d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyApplicationPathAndManagementPortTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present 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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +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 + */ +@AutoConfigureTestRestTemplate +@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/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java new file mode 100644 index 00000000000..b27538dfb06 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyDifferentPortSampleActuatorApplicationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present 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.resttestclient.TestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +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/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterApplicationTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterApplicationTests.java new file mode 100644 index 00000000000..b1ce5a44616 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterApplicationTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterManagementPortTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterManagementPortTests.java new file mode 100644 index 00000000000..e9da2b32e82 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyFilterManagementPortTests.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletApplicationTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletApplicationTests.java new file mode 100644 index 00000000000..ac188c8bc18 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletApplicationTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletManagementPortTests.java b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletManagementPortTests.java new file mode 100644 index 00000000000..a10049f3fba --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-jersey/src/test/java/smoketest/jersey/JerseyServletManagementPortTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-secure-jersey/build.gradle b/smoke-test/spring-boot-smoke-test-secure-jersey/build.gradle new file mode 100644 index 00000000000..a209da2aeda --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/build.gradle @@ -0,0 +1,29 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id "java" +} + +description = "Spring Boot secure Jersey smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-actuator")) + implementation(project(":starter:spring-boot-starter-jersey")) + implementation(project(":starter:spring-boot-starter-security")) + + testImplementation(project(":starter:spring-boot-starter-jersey-test")) +} diff --git a/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java new file mode 100644 index 00000000000..99e253ad8c8 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Endpoint.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java new file mode 100644 index 00000000000..3735f83b15d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/JerseyConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java new file mode 100644 index 00000000000..a858f2bd1cc --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/ReverseEndpoint.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java new file mode 100644 index 00000000000..d4ca03f4944 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SampleSecureJerseyApplication.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-present 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 +@SuppressWarnings("removal") +public class SampleSecureJerseyApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleSecureJerseyApplication.class, args); + } + + @Bean + TestServletEndpoint servletEndpoint() { + return new TestServletEndpoint(); + } + + @ServletEndpoint(id = "se1") + @SuppressWarnings("removal") + 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/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java new file mode 100644 index 00000000000..4d99a76e3e9 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/SecurityConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present 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.web.mappings.MappingsEndpoint; +import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +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 { + + @Bean + @SuppressWarnings("deprecation") + 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) { + http.authorizeHttpRequests((requests) -> { + requests.requestMatchers(EndpointRequest.to("health")).permitAll(); + requests.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)) + .hasRole("ACTUATOR"); + requests.requestMatchers("/**").hasRole("USER"); + }); + http.httpBasic(Customizer.withDefaults()); + return http.build(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java new file mode 100644 index 00000000000..be447809a8b --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/Service.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present 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}") + @SuppressWarnings("NullAway.Init") + private String msg; + + public String message() { + return this.msg; + } + +} diff --git a/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/package-info.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/package-info.java new file mode 100644 index 00000000000..2f4bdae42bd --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/java/smoketest/secure/jersey/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present 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. + */ + +@NullMarked +package smoketest.secure.jersey; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties b/smoke-test/spring-boot-smoke-test-secure-jersey/src/main/resources/application.properties new file mode 100644 index 00000000000..5d894fac2c8 --- /dev/null +++ b/smoke-test/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/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java new file mode 100644 index 00000000000..1c76932cdd7 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/AbstractJerseySecureTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-present 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.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +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 + */ +@AutoConfigureTestRestTemplate +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/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java new file mode 100644 index 00000000000..34a3025f805 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/CustomApplicationPathActuatorTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java new file mode 100644 index 00000000000..0a5fb286922 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/JerseySecureApplicationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-present 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/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java new file mode 100644 index 00000000000..1641ab8f8f2 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortAndPathJerseyApplicationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-present 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.resttestclient.TestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +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/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java new file mode 100644 index 00000000000..84442ce9097 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-secure-jersey/src/test/java/smoketest/secure/jersey/ManagementPortCustomApplicationPathJerseyTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present 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.resttestclient.TestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +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; + } + +} diff --git a/starter/spring-boot-starter-jersey-test/build.gradle b/starter/spring-boot-starter-jersey-test/build.gradle new file mode 100644 index 00000000000..d3eb694dfd7 --- /dev/null +++ b/starter/spring-boot-starter-jersey-test/build.gradle @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing JAX-RS and Jersey" + +dependencies { + api(project(":starter:spring-boot-starter-jersey")) + api(project(":starter:spring-boot-starter-test")) + api(project(":module:spring-boot-resttestclient")) +} + +checkRuntimeClasspathForConflicts { + ignore { name -> name.startsWith("org/aopalliance/intercept/") } + ignore { name -> name.startsWith("org/aopalliance/aop/") } +} diff --git a/starter/spring-boot-starter-jersey/build.gradle b/starter/spring-boot-starter-jersey/build.gradle new file mode 100644 index 00000000000..c530d23679c --- /dev/null +++ b/starter/spring-boot-starter-jersey/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using JAX-RS and Jersey" + +dependencies { + api(project(":starter:spring-boot-starter")) + api(project(":starter:spring-boot-starter-tomcat")) + api(project(":starter:spring-boot-starter-validation")) + + api(project(":module:spring-boot-jackson2")) + api(project(":module:spring-boot-jersey")) + + api("org.glassfish.jersey.ext:jersey-bean-validation") { + exclude group: "jakarta.el", module: "jakarta.el-api" + } + +} + +checkRuntimeClasspathForConflicts { + ignore { name -> name.startsWith("org/aopalliance/intercept/") } + ignore { name -> name.startsWith("org/aopalliance/aop/") } +}