Restore support for Jersey

Closes gh-28637
pull/32030/head
Andy Wilkinson 2 years ago
parent fb2f7c1e38
commit ba93e6c0ed

@ -177,10 +177,11 @@ public class DocumentConfigurationProperties extends DefaultTask {
prefix.accept("spring.graphql");
prefix.accept("spring.hateoas");
prefix.accept("spring.http");
prefix.accept("spring.servlet");
prefix.accept("spring.jersey");
prefix.accept("spring.mvc");
prefix.accept("spring.netty");
prefix.accept("spring.resources");
prefix.accept("spring.servlet");
prefix.accept("spring.session");
prefix.accept("spring.web");
prefix.accept("spring.webflux");

@ -107,6 +107,8 @@ dependencies {
exclude group: "commons-logging", module: "commons-logging"
}
optional("org.flywaydb:flyway-core")
optional("org.glassfish.jersey.core:jersey-server")
optional("org.glassfish.jersey.containers:jersey-container-servlet-core")
optional("org.hibernate.orm:hibernate-core")
optional("org.hibernate.orm:hibernate-micrometer")
optional("org.hibernate.validator:hibernate-validator")
@ -168,6 +170,8 @@ dependencies {
testImplementation("org.eclipse.jetty:jetty-webapp") {
exclude group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api"
}
testImplementation("org.glassfish.jersey.ext:jersey-spring6")
testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson")
testImplementation("org.hamcrest:hamcrest")
testImplementation("org.hsqldb:hsqldb")
testImplementation("org.junit.jupiter:junit-jupiter")

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,15 +16,19 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.endpoint.web.ExposableServletEndpoint;
import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
@ -63,4 +67,18 @@ public class ServletEndpointManagementContextConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
public static class JerseyServletEndpointManagementContextConfiguration {
@Bean
public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties,
ServletEndpointsSupplier servletEndpointsSupplier, JerseyApplicationPath jerseyApplicationPath) {
return new ServletEndpointRegistrar(jerseyApplicationPath.getRelativePath(properties.getBasePath()),
servletEndpointsSupplier.getEndpoints());
}
}
}

@ -0,0 +1,196 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.jersey.ManagementContextResourceConfigCustomizer;
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableServletEndpoint;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyHealthEndpointAdditionalPathResourceFactory;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Jersey
* {@link Endpoint @Endpoint} concerns.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Michael Simons
* @author Madhura Bhave
* @author HaiTao Zhang
*/
@ManagementContextConfiguration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnBean(WebEndpointsSupplier.class)
@ConditionalOnMissingBean(type = "org.springframework.web.servlet.DispatcherServlet")
class JerseyWebEndpointManagementContextConfiguration {
private static final EndpointId HEALTH_ENDPOINT_ID = EndpointId.of("health");
@Bean
JerseyWebEndpointsResourcesRegistrar jerseyWebEndpointsResourcesRegistrar(Environment environment,
WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, WebEndpointProperties webEndpointProperties) {
String basePath = webEndpointProperties.getBasePath();
boolean shouldRegisterLinks = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new JerseyWebEndpointsResourcesRegistrar(webEndpointsSupplier, servletEndpointsSupplier,
endpointMediaTypes, basePath, shouldRegisterLinks);
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
@ConditionalOnBean(HealthEndpoint.class)
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentPortAdditionalHealthEndpointPathsResourcesRegistrar(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint health = webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HEALTH_ENDPOINT_ID)).findFirst().get();
return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment,
String basePath) {
return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
/**
* Register endpoints with the {@link ResourceConfig} for the management context.
*/
static class JerseyWebEndpointsResourcesRegistrar implements ManagementContextResourceConfigCustomizer {
private final WebEndpointsSupplier webEndpointsSupplier;
private final ServletEndpointsSupplier servletEndpointsSupplier;
private final EndpointMediaTypes mediaTypes;
private final String basePath;
private final boolean shouldRegisterLinks;
JerseyWebEndpointsResourcesRegistrar(WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier, EndpointMediaTypes endpointMediaTypes,
String basePath, boolean shouldRegisterLinks) {
this.webEndpointsSupplier = webEndpointsSupplier;
this.servletEndpointsSupplier = servletEndpointsSupplier;
this.mediaTypes = endpointMediaTypes;
this.basePath = basePath;
this.shouldRegisterLinks = shouldRegisterLinks;
}
@Override
public void customize(ResourceConfig config) {
register(config);
}
private void register(ResourceConfig config) {
Collection<ExposableWebEndpoint> webEndpoints = this.webEndpointsSupplier.getEndpoints();
Collection<ExposableServletEndpoint> servletEndpoints = this.servletEndpointsSupplier.getEndpoints();
EndpointLinksResolver linksResolver = getLinksResolver(webEndpoints, servletEndpoints);
EndpointMapping mapping = new EndpointMapping(this.basePath);
Collection<Resource> endpointResources = new JerseyEndpointResourceFactory().createEndpointResources(
mapping, webEndpoints, this.mediaTypes, linksResolver, this.shouldRegisterLinks);
register(endpointResources, config);
}
private EndpointLinksResolver getLinksResolver(Collection<ExposableWebEndpoint> webEndpoints,
Collection<ExposableServletEndpoint> servletEndpoints) {
List<ExposableEndpoint<?>> endpoints = new ArrayList<>(webEndpoints.size() + servletEndpoints.size());
endpoints.addAll(webEndpoints);
endpoints.addAll(servletEndpoints);
return new EndpointLinksResolver(endpoints, this.basePath);
}
private void register(Collection<Resource> resources, ResourceConfig config) {
config.registerResources(new HashSet<>(resources));
}
}
class JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar
implements ManagementContextResourceConfigCustomizer {
private final ExposableWebEndpoint endpoint;
private final HealthEndpointGroups groups;
JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(ExposableWebEndpoint endpoint,
HealthEndpointGroups groups) {
this.endpoint = endpoint;
this.groups = groups;
}
@Override
public void customize(ResourceConfig config) {
register(config);
}
private void register(ResourceConfig config) {
EndpointMapping mapping = new EndpointMapping("");
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.MANAGEMENT, this.groups);
Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false)
.stream().filter(Objects::nonNull).collect(Collectors.toList());
register(endpointResources, config);
}
private void register(Collection<Resource> resources, ResourceConfig config) {
config.registerResources(new HashSet<>(resources));
}
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for exposing actuator web endpoints using Jersey.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;

@ -17,21 +17,40 @@
package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.stream.Collectors;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.servlet.ServletContainer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyHealthEndpointAdditionalPathResourceFactory;
import org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jersey.JerseyProperties;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
@ -78,4 +97,79 @@ class HealthEndpointWebExtensionConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
static class JerseyAdditionalHealthEndpointPathsConfiguration {
@Bean
JerseyAdditionalHealthEndpointPathsResourcesRegistrar jerseyAdditionalHealthEndpointPathsResourcesRegistrar(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) {
ExposableWebEndpoint health = getHealthEndpoint(webEndpointsSupplier);
return new JerseyAdditionalHealthEndpointPathsResourcesRegistrar(health, healthEndpointGroups);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ResourceConfig.class)
@EnableConfigurationProperties(JerseyProperties.class)
static class JerseyInfrastructureConfiguration {
@Bean
@ConditionalOnMissingBean(JerseyApplicationPath.class)
JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties, ResourceConfig config) {
return new DefaultJerseyApplicationPath(properties.getApplicationPath(), config);
}
@Bean
ResourceConfig resourceConfig(ObjectProvider<ResourceConfigCustomizer> resourceConfigCustomizers) {
ResourceConfig resourceConfig = new ResourceConfig();
resourceConfigCustomizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig));
return resourceConfig;
}
@Bean
ServletRegistrationBean<ServletContainer> jerseyServletRegistration(
JerseyApplicationPath jerseyApplicationPath, ResourceConfig resourceConfig) {
return new ServletRegistrationBean<>(new ServletContainer(resourceConfig),
jerseyApplicationPath.getUrlMapping());
}
}
}
static class JerseyAdditionalHealthEndpointPathsResourcesRegistrar implements ResourceConfigCustomizer {
private final ExposableWebEndpoint endpoint;
private final HealthEndpointGroups groups;
JerseyAdditionalHealthEndpointPathsResourcesRegistrar(ExposableWebEndpoint endpoint,
HealthEndpointGroups groups) {
this.endpoint = endpoint;
this.groups = groups;
}
@Override
public void customize(ResourceConfig config) {
register(config);
}
private void register(ResourceConfig config) {
EndpointMapping mapping = new EndpointMapping("");
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.SERVER, this.groups);
Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false)
.stream().filter(Objects::nonNull).collect(Collectors.toList());
register(endpointResources, config);
}
private void register(Collection<Resource> resources, ResourceConfig config) {
config.registerResources(new HashSet<>(resources));
}
}
}

@ -0,0 +1,105 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.jersey;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jersey.server.AnnotationFinder;
import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider;
import io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider;
import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener;
import io.micrometer.core.instrument.config.MeterFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server;
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Jersey server instrumentation.
*
* @author Michael Weirauch
* @author Michael Simons
* @author Andy Wilkinson
* @since 2.1.0
*/
@AutoConfiguration(after = { MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ ResourceConfig.class, MetricsApplicationEventListener.class })
@ConditionalOnBean({ MeterRegistry.class, ResourceConfig.class })
@EnableConfigurationProperties(MetricsProperties.class)
public class JerseyServerMetricsAutoConfiguration {
private final MetricsProperties properties;
public JerseyServerMetricsAutoConfiguration(MetricsProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(JerseyTagsProvider.class)
public DefaultJerseyTagsProvider jerseyTagsProvider() {
return new DefaultJerseyTagsProvider();
}
@Bean
public ResourceConfigCustomizer jerseyServerMetricsResourceConfigCustomizer(MeterRegistry meterRegistry,
JerseyTagsProvider tagsProvider) {
Server server = this.properties.getWeb().getServer();
return (config) -> config.register(
new MetricsApplicationEventListener(meterRegistry, tagsProvider, server.getRequest().getMetricName(),
server.getRequest().getAutotime().isEnabled(), new AnnotationUtilsAnnotationFinder()));
}
@Bean
@Order(0)
public MeterFilter jerseyMetricsUriTagFilter() {
String metricName = this.properties.getWeb().getServer().getRequest().getMetricName();
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
() -> String.format("Reached the maximum number of URI tags for '%s'.", metricName));
return MeterFilter.maximumAllowableTags(metricName, "uri", this.properties.getWeb().getServer().getMaxUriTags(),
filter);
}
/**
* An {@link AnnotationFinder} that uses {@link AnnotationUtils}.
*/
private static class AnnotationUtilsAnnotationFinder implements AnnotationFinder {
@Override
public <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType) {
return AnnotationUtils.findAnnotation(annotatedElement, annotationType);
}
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for Jersey actuator metrics.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.jersey;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,14 +16,18 @@
package org.springframework.boot.actuate.autoconfigure.security.servlet;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider;
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.util.matcher.RequestMatcher;
@ -55,4 +59,17 @@ public class SecurityRequestMatchersManagementContextConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
@ConditionalOnBean(JerseyApplicationPath.class)
public static class JerseyRequestMatcherConfiguration {
@Bean
public RequestMatcherProvider requestMatcherProvider(JerseyApplicationPath applicationPath) {
return new AntPathRequestMatcherProvider(applicationPath::getRelativePath);
}
}
}

@ -0,0 +1,58 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Jersey
* infrastructure when a separate management context with a web server running on a
* different port is required.
*
* @author Madhura Bhave
* @since 2.1.0
*/
@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false)
@Import(JerseyManagementContextConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
public class JerseyChildManagementContextConfiguration {
@Bean
public JerseyApplicationPath jerseyApplicationPath() {
return () -> "/";
}
@Bean
ResourceConfig resourceConfig(ObjectProvider<ManagementContextResourceConfigCustomizer> customizers) {
ResourceConfig resourceConfig = new ResourceConfig();
customizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig));
return resourceConfig;
}
}

@ -0,0 +1,42 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Shared configuration for Jersey-based actuators regardless of management context type.
*
* @author Madhura Bhave
*/
@Configuration(proxyBeanMethods = false)
class JerseyManagementContextConfiguration {
@Bean
ServletRegistrationBean<ServletContainer> jerseyServletRegistration(JerseyApplicationPath jerseyApplicationPath,
ResourceConfig resourceConfig) {
return new ServletRegistrationBean<>(new ServletContainer(resourceConfig),
jerseyApplicationPath.getUrlMapping());
}
}

@ -0,0 +1,77 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.jersey.JerseyProperties;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Jersey
* infrastructure when the management context is the same as the main application context.
*
* @author Madhura Bhave
* @since 2.1.0
*/
@ManagementContextConfiguration(value = ManagementContextType.SAME, proxyBeanMethods = false)
@EnableConfigurationProperties(JerseyProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
public class JerseySameManagementContextConfiguration {
@Bean
ResourceConfigCustomizer managementResourceConfigCustomizerAdapter(
ObjectProvider<ManagementContextResourceConfigCustomizer> customizers) {
return (config) -> customizers.orderedStream().forEach((customizer) -> customizer.customize(config));
}
@Configuration(proxyBeanMethods = false)
@Import(JerseyManagementContextConfiguration.class)
@ConditionalOnMissingBean(ResourceConfig.class)
static class JerseyInfrastructureConfiguration {
@Bean
@ConditionalOnMissingBean(JerseyApplicationPath.class)
JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties, ResourceConfig config) {
return new DefaultJerseyApplicationPath(properties.getApplicationPath(), config);
}
@Bean
ResourceConfig resourceConfig(ObjectProvider<ResourceConfigCustomizer> resourceConfigCustomizers) {
ResourceConfig resourceConfig = new ResourceConfig();
resourceConfigCustomizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig));
return resourceConfig;
}
}
}

@ -0,0 +1,36 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.jersey;
import org.glassfish.jersey.server.ResourceConfig;
/**
* Callback interface that can be implemented by beans wishing to customize Jersey's
* {@link ResourceConfig} in the management context before it is used.
*
* @author Andy Wilkinson
* @since 2.3.10
*/
public interface ManagementContextResourceConfigCustomizer {
/**
* Customize the resource config.
* @param config the {@link ResourceConfig} to customize
*/
void customize(ResourceConfig config);
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Configuration for a Jersey-based management context.
*/
package org.springframework.boot.actuate.autoconfigure.web.jersey;

@ -1,7 +1,10 @@
org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration
org.springframework.boot.actuate.autoconfigure.security.servlet.SecurityRequestMatchersManagementContextConfiguration
org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration
org.springframework.boot.actuate.autoconfigure.web.jersey.JerseyChildManagementContextConfiguration
org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementChildContextConfiguration
org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementChildContextConfiguration
org.springframework.boot.actuate.autoconfigure.web.servlet.WebMvcEndpointChildContextConfiguration

@ -70,6 +70,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.Wavefron
org.springframework.boot.actuate.autoconfigure.metrics.graphql.GraphQlMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,17 +18,21 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web;
import java.util.Collections;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
@ -45,13 +49,24 @@ class ServletEndpointManagementContextConfigurationTests {
@Test
void contextShouldContainServletEndpointRegistrar() {
this.contextRunner.run((context) -> {
FilteredClassLoader classLoader = new FilteredClassLoader(ResourceConfig.class);
this.contextRunner.withClassLoader(classLoader).run((context) -> {
assertThat(context).hasSingleBean(ServletEndpointRegistrar.class);
ServletEndpointRegistrar bean = context.getBean(ServletEndpointRegistrar.class);
assertThat(bean).hasFieldOrPropertyWithValue("basePath", "/test/actuator");
});
}
@Test
void contextWhenJerseyShouldContainServletEndpointRegistrar() {
FilteredClassLoader classLoader = new FilteredClassLoader(DispatcherServlet.class);
this.contextRunner.withClassLoader(classLoader).run((context) -> {
assertThat(context).hasSingleBean(ServletEndpointRegistrar.class);
ServletEndpointRegistrar bean = context.getBean(ServletEndpointRegistrar.class);
assertThat(bean).hasFieldOrPropertyWithValue("basePath", "/jersey/actuator");
});
}
@Test
void contextWhenNoServletBasedShouldNotContainServletEndpointRegistrar() {
new ApplicationContextRunner().withUserConfiguration(TestConfig.class)
@ -73,6 +88,11 @@ class ServletEndpointManagementContextConfigurationTests {
return () -> "/test";
}
@Bean
JerseyApplicationPath jerseyApplicationPath() {
return () -> "/jersey";
}
}
}

@ -0,0 +1,75 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;
import java.util.Set;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for web endpoints running on Jersey.
*
* @author Andy Wilkinson
*/
class JerseyWebEndpointIntegrationTests {
@Test
void whenJerseyIsConfiguredToUseAFilterThenResourceRegistrationSucceeds() {
new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class,
JerseyAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
JerseyWebEndpointManagementContextConfiguration.class))
.withUserConfiguration(ResourceConfigConfiguration.class)
.withClassLoader(new FilteredClassLoader(DispatcherServlet.class))
.withPropertyValues("spring.jersey.type=filter", "server.port=0").run((context) -> {
assertThat(context).hasNotFailed();
Set<Resource> 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();
}
}
}

@ -0,0 +1,67 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;
import java.util.Collections;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration.JerseyWebEndpointsResourcesRegistrar;
import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyWebEndpointManagementContextConfiguration}.
*
* @author Michael Simons
* @author Madhura Bhave
*/
class JerseyWebEndpointManagementContextConfigurationTests {
private final WebApplicationContextRunner runner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebEndpointAutoConfiguration.class,
JerseyWebEndpointManagementContextConfiguration.class))
.withBean(WebEndpointsSupplier.class, () -> Collections::emptyList);
@Test
void jerseyWebEndpointsResourcesRegistrarForEndpointsIsAutoConfigured() {
this.runner.run((context) -> assertThat(context).hasSingleBean(JerseyWebEndpointsResourcesRegistrar.class));
}
@Test
void autoConfigurationIsConditionalOnServletWebApplication() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class));
contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class));
}
@Test
void autoConfigurationIsConditionalOnClassResourceConfig() {
this.runner.withClassLoader(new FilteredClassLoader(ResourceConfig.class))
.run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class));
}
}

@ -0,0 +1,159 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.integrationtest;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Integration tests for the Jersey actuator endpoints.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
class JerseyEndpointIntegrationTests {
@Test
void linksAreProvidedToAllEndpointTypes() {
testJerseyEndpoints(new Class<?>[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class });
}
@Test
void linksPageIsNotAvailableWhenDisabled() {
getContextRunner(new Class<?>[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class })
.withPropertyValues("management.endpoints.web.discovery.enabled:false").run((context) -> {
int port = context
.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class)
.getWebServer().getPort();
WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port)
.responseTimeout(Duration.ofMinutes(5)).build();
client.get().uri("/actuator").exchange().expectStatus().isNotFound();
});
}
@Test
void actuatorEndpointsWhenUserProvidedResourceConfigBeanNotAvailable() {
testJerseyEndpoints(new Class<?>[] { EndpointsConfiguration.class });
}
@Test
void actuatorEndpointsWhenSecurityAvailable() {
WebApplicationContextRunner contextRunner = getContextRunner(
new Class<?>[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class },
getAutoconfigurations(SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class));
contextRunner.run((context) -> {
int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class)
.getWebServer().getPort();
WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port)
.responseTimeout(Duration.ofMinutes(5)).build();
client.get().uri("/actuator").exchange().expectStatus().isUnauthorized();
});
}
protected void testJerseyEndpoints(Class<?>[] userConfigurations) {
getContextRunner(userConfigurations).run((context) -> {
int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class)
.getWebServer().getPort();
WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port)
.responseTimeout(Duration.ofMinutes(5)).build();
client.get().uri("/actuator").exchange().expectStatus().isOk().expectBody().jsonPath("_links.beans")
.isNotEmpty().jsonPath("_links.restcontroller").doesNotExist().jsonPath("_links.controller")
.doesNotExist();
});
}
WebApplicationContextRunner getContextRunner(Class<?>[] userConfigurations,
Class<?>... additionalAutoConfigurations) {
FilteredClassLoader classLoader = new FilteredClassLoader(DispatcherServlet.class);
return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withClassLoader(classLoader)
.withConfiguration(AutoConfigurations.of(getAutoconfigurations(additionalAutoConfigurations)))
.withUserConfiguration(userConfigurations)
.withPropertyValues("management.endpoints.web.exposure.include:*", "server.port:0");
}
private Class<?>[] getAutoconfigurations(Class<?>... additional) {
List<Class<?>> autoconfigurations = new ArrayList<>(Arrays.asList(JacksonAutoConfiguration.class,
JerseyAutoConfiguration.class, EndpointAutoConfiguration.class,
ServletWebServerFactoryAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ManagementContextAutoConfiguration.class, BeansEndpointAutoConfiguration.class));
autoconfigurations.addAll(Arrays.asList(additional));
return autoconfigurations.toArray(new Class<?>[0]);
}
@ControllerEndpoint(id = "controller")
static class TestControllerEndpoint {
}
@RestControllerEndpoint(id = "restcontroller")
static class TestRestControllerEndpoint {
}
@Configuration(proxyBeanMethods = false)
static class EndpointsConfiguration {
@Bean
TestControllerEndpoint testControllerEndpoint() {
return new TestControllerEndpoint();
}
@Bean
TestRestControllerEndpoint testRestControllerEndpoint() {
return new TestRestControllerEndpoint();
}
}
@Configuration(proxyBeanMethods = false)
static class ResourceConfigConfiguration {
@Bean
ResourceConfig testResourceConfig() {
return new ResourceConfig();
}
}
}

@ -0,0 +1,56 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.integrationtest;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Integration tests for health groups on an additional path on Jersey.
*
* @author Madhura Bhave
*/
class JerseyHealthEndpointAdditionalPathIntegrationTests extends
AbstractHealthEndpointAdditionalPathIntegrationTests<WebApplicationContextRunner, ConfigurableWebApplicationContext, AssertableWebApplicationContext> {
JerseyHealthEndpointAdditionalPathIntegrationTests() {
super(new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, JerseyAutoConfiguration.class,
EndpointAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
WebEndpointAutoConfiguration.class, JerseyAutoConfiguration.class,
ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
HealthEndpointAutoConfiguration.class, DiskSpaceHealthContributorAutoConfiguration.class))
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withClassLoader(new FilteredClassLoader(DispatcherServlet.class)).withPropertyValues("server.port=0"));
}
}

@ -0,0 +1,160 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.jersey;
import java.net.URI;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider;
import io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider;
import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyServerMetricsAutoConfiguration}.
*
* @author Michael Weirauch
* @author Michael Simons
*/
class JerseyServerMetricsAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
.withConfiguration(AutoConfigurations.of(JerseyServerMetricsAutoConfiguration.class));
private final WebApplicationContextRunner webContextRunner = new WebApplicationContextRunner(
AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class,
JerseyServerMetricsAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, MetricsAutoConfiguration.class))
.withUserConfiguration(ResourceConfiguration.class).withPropertyValues("server.port:0");
@Test
void shouldOnlyBeActiveInWebApplicationContext() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ResourceConfigCustomizer.class));
}
@Test
void shouldProvideAllNecessaryBeans() {
this.webContextRunner.run((context) -> assertThat(context).hasSingleBean(DefaultJerseyTagsProvider.class)
.hasSingleBean(ResourceConfigCustomizer.class));
}
@Test
void shouldHonorExistingTagProvider() {
this.webContextRunner.withUserConfiguration(CustomJerseyTagsProviderConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(CustomJerseyTagsProvider.class));
}
@Test
void httpRequestsAreTimed() {
this.webContextRunner.run((context) -> {
doRequest(context);
MeterRegistry registry = context.getBean(MeterRegistry.class);
Timer timer = registry.get("http.server.requests").tag("uri", "/users/{id}").timer();
assertThat(timer.count()).isEqualTo(1);
});
}
@Test
void noHttpRequestsTimedWhenJerseyInstrumentationMissingFromClasspath() {
this.webContextRunner.withClassLoader(new FilteredClassLoader(MetricsApplicationEventListener.class))
.run((context) -> {
doRequest(context);
MeterRegistry registry = context.getBean(MeterRegistry.class);
assertThat(registry.find("http.server.requests").timer()).isNull();
});
}
private static void doRequest(AssertableWebApplicationContext context) {
int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class)
.getWebServer().getPort();
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity(URI.create("http://localhost:" + port + "/users/3"), String.class);
}
@Configuration(proxyBeanMethods = false)
static class ResourceConfiguration {
@Bean
ResourceConfig resourceConfig() {
return new ResourceConfig().register(new TestResource());
}
@Path("/users")
public class TestResource {
@GET
@Path("/{id}")
public String getUser(@PathParam("id") String id) {
return id;
}
}
}
@Configuration(proxyBeanMethods = false)
static class CustomJerseyTagsProviderConfiguration {
@Bean
JerseyTagsProvider customJerseyTagsProvider() {
return new CustomJerseyTagsProvider();
}
}
static class CustomJerseyTagsProvider implements JerseyTagsProvider {
@Override
public Iterable<Tag> httpRequestTags(RequestEvent event) {
return null;
}
@Override
public Iterable<Tag> httpLongRequestTags(RequestEvent event) {
return null;
}
}
}

@ -43,6 +43,7 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.test.web.reactive.server.WebTestClient;
@ -76,10 +77,15 @@ abstract class AbstractEndpointRequestIntegrationTests {
getContextRunner().run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/actuator").exchange().expectStatus().isOk();
webTestClient.get().uri("/actuator/").exchange().expectStatus().isNotFound();
webTestClient.get().uri("/actuator/").exchange().expectStatus()
.isEqualTo(expectedStatusWithTrailingSlash());
});
}
protected HttpStatus expectedStatusWithTrailingSlash() {
return HttpStatus.NOT_FOUND;
}
protected final WebApplicationContextRunner getContextRunner() {
return createContextRunner().withPropertyValues("management.endpoints.web.exposure.include=*")
.withUserConfiguration(BaseConfiguration.class, SecurityConfiguration.class).withConfiguration(

@ -0,0 +1,130 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.security.servlet;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration tests for {@link EndpointRequest} with Jersey.
*
* @author Madhura Bhave
*/
class JerseyEndpointRequestIntegrationTests extends AbstractEndpointRequestIntegrationTests {
@Test
void toLinksWhenApplicationPathSetShouldMatch() {
getContextRunner().withPropertyValues("spring.jersey.application-path=/admin").run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/admin/actuator/").exchange().expectStatus()
.isEqualTo(expectedStatusWithTrailingSlash());
webTestClient.get().uri("/admin/actuator").exchange().expectStatus().isOk();
});
}
@Test
void toEndpointWhenApplicationPathSetShouldMatch() {
getContextRunner().withPropertyValues("spring.jersey.application-path=/admin").run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/admin/actuator/e1").exchange().expectStatus().isOk();
});
}
@Test
void toAnyEndpointWhenApplicationPathSetShouldMatch() {
getContextRunner()
.withPropertyValues("spring.jersey.application-path=/admin", "spring.security.user.password=password")
.run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/admin/actuator/e2").exchange().expectStatus().isUnauthorized();
webTestClient.get().uri("/admin/actuator/e2").header("Authorization", getBasicAuth()).exchange()
.expectStatus().isOk();
});
}
@Test
void toAnyEndpointShouldMatchServletEndpoint() {
getContextRunner().withPropertyValues("spring.security.user.password=password",
"management.endpoints.web.exposure.include=se1").run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/actuator/se1").exchange().expectStatus().isUnauthorized();
webTestClient.get().uri("/actuator/se1").header("Authorization", getBasicAuth()).exchange()
.expectStatus().isOk();
webTestClient.get().uri("/actuator/se1/list").exchange().expectStatus().isUnauthorized();
webTestClient.get().uri("/actuator/se1/list").header("Authorization", getBasicAuth()).exchange()
.expectStatus().isOk();
});
}
@Test
void toAnyEndpointWhenApplicationPathSetShouldMatchServletEndpoint() {
getContextRunner().withPropertyValues("spring.jersey.application-path=/admin",
"spring.security.user.password=password", "management.endpoints.web.exposure.include=se1")
.run((context) -> {
WebTestClient webTestClient = getWebTestClient(context);
webTestClient.get().uri("/admin/actuator/se1").exchange().expectStatus().isUnauthorized();
webTestClient.get().uri("/admin/actuator/se1").header("Authorization", getBasicAuth()).exchange()
.expectStatus().isOk();
webTestClient.get().uri("/admin/actuator/se1/list").exchange().expectStatus().isUnauthorized();
webTestClient.get().uri("/admin/actuator/se1/list").header("Authorization", getBasicAuth())
.exchange().expectStatus().isOk();
});
}
@Override
protected HttpStatus expectedStatusWithTrailingSlash() {
return HttpStatus.OK;
}
@Override
protected WebApplicationContextRunner createContextRunner() {
return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet"))
.withUserConfiguration(JerseyEndpointConfiguration.class)
.withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class));
}
@Configuration
@EnableConfigurationProperties(WebEndpointProperties.class)
static class JerseyEndpointConfiguration {
@Bean
TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(0);
}
@Bean
ResourceConfig resourceConfig() {
return new ResourceConfig();
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.servlet.AntPathRequestMatcherProvider;
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
@ -66,6 +67,17 @@ class SecurityRequestMatchersManagementContextConfigurationTests {
});
}
@Test
void registersRequestMatcherForJerseyProviderIfJerseyPresentAndMvcAbsent() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet"))
.withUserConfiguration(TestJerseyConfiguration.class).run((context) -> {
AntPathRequestMatcherProvider matcherProvider = context
.getBean(AntPathRequestMatcherProvider.class);
RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example");
assertThat(requestMatcher).extracting("pattern").isEqualTo("/admin/example");
});
}
@Test
void mvcRequestMatcherProviderConditionalOnDispatcherServletClass() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet"))
@ -79,6 +91,20 @@ class SecurityRequestMatchersManagementContextConfigurationTests {
.run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class));
}
@Test
void jerseyRequestMatcherProviderConditionalOnResourceConfigClass() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.glassfish.jersey.server.ResourceConfig"))
.run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class));
}
@Test
void jerseyRequestMatcherProviderConditionalOnJerseyApplicationPathBean() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class))
.withClassLoader(new FilteredClassLoader("org.springframework.web.servlet.DispatcherServlet"))
.run((context) -> assertThat(context).doesNotHaveBean(AntPathRequestMatcherProvider.class));
}
@Configuration(proxyBeanMethods = false)
static class TestMvcConfiguration {
@ -89,4 +115,14 @@ class SecurityRequestMatchersManagementContextConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class TestJerseyConfiguration {
@Bean
JerseyApplicationPath jerseyApplicationPath() {
return () -> "/admin";
}
}
}

@ -0,0 +1,106 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JerseyChildManagementContextConfiguration}.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
@ClassPathExclusions("spring-webmvc-*")
class JerseyChildManagementContextConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withUserConfiguration(JerseyChildManagementContextConfiguration.class);
@Test
void autoConfigurationIsConditionalOnServletWebApplication() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class));
contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class));
}
@Test
void autoConfigurationIsConditionalOnClassResourceConfig() {
this.contextRunner.withClassLoader(new FilteredClassLoader(ResourceConfig.class))
.run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class));
}
@Test
void jerseyApplicationPathIsAutoConfigured() {
this.contextRunner.run((context) -> {
JerseyApplicationPath bean = context.getBean(JerseyApplicationPath.class);
assertThat(bean.getPath()).isEqualTo("/");
});
}
@Test
@SuppressWarnings("unchecked")
void servletRegistrationBeanIsAutoConfigured() {
this.contextRunner.run((context) -> {
ServletRegistrationBean<ServletContainer> 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);
}
}
}

@ -0,0 +1,136 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JerseySameManagementContextConfiguration}.
*
* @author Madhura Bhave
*/
@ClassPathExclusions("spring-webmvc-*")
class JerseySameManagementContextConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class));
@Test
void autoConfigurationIsConditionalOnServletWebApplication() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class));
contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class));
}
@Test
void autoConfigurationIsConditionalOnClassResourceConfig() {
this.contextRunner.withClassLoader(new FilteredClassLoader(ResourceConfig.class))
.run((context) -> assertThat(context).doesNotHaveBean(JerseySameManagementContextConfiguration.class));
}
@Test
void jerseyApplicationPathIsAutoConfiguredWhenNeeded() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DefaultJerseyApplicationPath.class));
}
@Test
void jerseyApplicationPathIsConditionalOnMissingBean() {
this.contextRunner.withUserConfiguration(ConfigWithJerseyApplicationPath.class).run((context) -> {
assertThat(context).hasSingleBean(JerseyApplicationPath.class);
assertThat(context).hasBean("testJerseyApplicationPath");
});
}
@Test
void existingResourceConfigBeanShouldNotAutoConfigureRelatedBeans() {
this.contextRunner.withUserConfiguration(ConfigWithResourceConfig.class).run((context) -> {
assertThat(context).hasSingleBean(ResourceConfig.class);
assertThat(context).doesNotHaveBean(JerseyApplicationPath.class);
assertThat(context).doesNotHaveBean(ServletRegistrationBean.class);
assertThat(context).hasBean("customResourceConfig");
});
}
@Test
@SuppressWarnings("unchecked")
void servletRegistrationBeanIsAutoConfiguredWhenNeeded() {
this.contextRunner.withPropertyValues("spring.jersey.application-path=/jersey").run((context) -> {
ServletRegistrationBean<ServletContainer> 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);
}
}
}

@ -70,6 +70,10 @@ class ManagementContextConfigurationImportSelectorTests {
.load(ManagementContextConfiguration.class,
ManagementContextConfigurationImportSelectorTests.class.getClassLoader())
.forEach(expected::add);
// Remove JerseySameManagementContextConfiguration, as it specifies
// ManagementContextType.SAME and we asked for ManagementContextType.CHILD
expected.remove(
"org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration");
assertThat(imports).containsExactlyInAnyOrderElementsOf(expected);
}

@ -47,6 +47,8 @@ dependencies {
exclude(group: "commons-logging", module: "commons-logging")
}
optional("org.flywaydb:flyway-core")
optional("org.glassfish.jersey.core:jersey-server")
optional("org.glassfish.jersey.containers:jersey-container-servlet-core")
optional("org.hibernate.validator:hibernate-validator")
optional("org.influxdb:influxdb-java")
optional("org.liquibase:liquibase-core") {
@ -88,6 +90,7 @@ dependencies {
testImplementation("io.r2dbc:r2dbc-h2")
testImplementation("org.apache.logging.log4j:log4j-to-slf4j")
testImplementation("org.awaitility:awaitility")
testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson")
testImplementation("org.hamcrest:hamcrest")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core")
@ -101,5 +104,6 @@ dependencies {
testRuntimeOnly("io.projectreactor.netty:reactor-netty-http")
testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api")
testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-el")
testRuntimeOnly("org.glassfish.jersey.ext:jersey-spring6")
testRuntimeOnly("org.hsqldb:hsqldb")
}

@ -0,0 +1,356 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.web.jersey;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.Resource.Builder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* A factory for creating Jersey {@link Resource Resources} for {@link WebOperation web
* endpoint operations}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public class JerseyEndpointResourceFactory {
/**
* Creates {@link Resource Resources} for the operations of the given
* {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param linksResolver resolver for determining links to available endpoints
* @param shouldRegisterLinks should register links
* @return the resources for the operations
*/
public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
EndpointLinksResolver linksResolver, boolean shouldRegisterLinks) {
List<Resource> resources = new ArrayList<>();
endpoints.stream().flatMap((endpoint) -> endpoint.getOperations().stream())
.map((operation) -> createResource(endpointMapping, operation)).forEach(resources::add);
if (shouldRegisterLinks) {
Resource resource = createEndpointLinksResource(endpointMapping.getPath(), endpointMediaTypes,
linksResolver);
resources.add(resource);
}
return resources;
}
protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String path = requestPredicate.getPath();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}",
"{" + matchAllRemainingPathSegmentsVariable + ": .*}");
}
return getResource(endpointMapping, operation, requestPredicate, path, null, null);
}
protected Resource getResource(EndpointMapping endpointMapping, WebOperation operation,
WebOperationRequestPredicate requestPredicate, String path, WebServerNamespace serverNamespace,
JerseyRemainingPathSegmentProvider remainingPathSegmentProvider) {
Builder resourceBuilder = Resource.builder().path(endpointMapping.getPath())
.path(endpointMapping.createSubPath(path));
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
.consumes(StringUtils.toStringArray(requestPredicate.getConsumes()))
.produces(StringUtils.toStringArray(requestPredicate.getProduces()))
.handledBy(new OperationInflector(operation, !requestPredicate.getConsumes().isEmpty(), serverNamespace,
remainingPathSegmentProvider));
return resourceBuilder.build();
}
private Resource createEndpointLinksResource(String endpointPath, EndpointMediaTypes endpointMediaTypes,
EndpointLinksResolver linksResolver) {
Builder resourceBuilder = Resource.builder().path(endpointPath);
resourceBuilder.addMethod("GET").produces(StringUtils.toStringArray(endpointMediaTypes.getProduced()))
.handledBy(new EndpointLinksInflector(linksResolver));
return resourceBuilder.build();
}
/**
* {@link Inflector} to invoke the {@link WebOperation}.
*/
private static final class OperationInflector implements Inflector<ContainerRequestContext, Object> {
private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
private static final List<Function<Object, Object>> BODY_CONVERTERS;
static {
List<Function<Object, Object>> converters = new ArrayList<>();
converters.add(new ResourceBodyConverter());
if (ClassUtils.isPresent("reactor.core.publisher.Mono", OperationInflector.class.getClassLoader())) {
converters.add(new FluxBodyConverter());
converters.add(new MonoBodyConverter());
}
BODY_CONVERTERS = Collections.unmodifiableList(converters);
}
private final WebOperation operation;
private final boolean readBody;
private final WebServerNamespace serverNamespace;
private final JerseyRemainingPathSegmentProvider remainingPathSegmentProvider;
private OperationInflector(WebOperation operation, boolean readBody, WebServerNamespace serverNamespace,
JerseyRemainingPathSegmentProvider remainingPathSegments) {
this.operation = operation;
this.readBody = readBody;
this.serverNamespace = serverNamespace;
this.remainingPathSegmentProvider = remainingPathSegments;
}
@Override
public Response apply(ContainerRequestContext data) {
Map<String, Object> 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<String, Object> extractBodyArguments(ContainerRequestContext data) {
Map<String, Object> entity = ((ContainerRequest) data).readEntity(Map.class);
return (entity != null) ? entity : Collections.emptyMap();
}
private Map<String, Object> extractPathParameters(ContainerRequestContext requestContext) {
Map<String, Object> pathParameters = extract(requestContext.getUriInfo().getPathParameters());
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
String remainingPathSegments = getRemainingPathSegments(requestContext, pathParameters,
matchAllRemainingPathSegmentsVariable);
pathParameters.put(matchAllRemainingPathSegmentsVariable, tokenizePathSegments(remainingPathSegments));
}
return pathParameters;
}
private String getRemainingPathSegments(ContainerRequestContext requestContext,
Map<String, Object> pathParameters, String matchAllRemainingPathSegmentsVariable) {
if (this.remainingPathSegmentProvider != null) {
return this.remainingPathSegmentProvider.get(requestContext, matchAllRemainingPathSegmentsVariable);
}
return (String) pathParameters.get(matchAllRemainingPathSegmentsVariable);
}
private String[] tokenizePathSegments(String path) {
String[] segments = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR, false, true);
for (int i = 0; i < segments.length; i++) {
if (segments[i].contains("%")) {
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
}
}
return segments;
}
private Map<String, Object> extractQueryParameters(ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getQueryParameters());
}
private Map<String, Object> extract(MultivaluedMap<String, String> multivaluedMap) {
Map<String, Object> result = new HashMap<>();
multivaluedMap.forEach((name, values) -> {
if (!CollectionUtils.isEmpty(values)) {
result.put(name, (values.size() != 1) ? values : values.get(0));
}
});
return result;
}
private Response convertToJaxRsResponse(Object response, String httpMethod) {
if (response == null) {
boolean isGet = HttpMethod.GET.equals(httpMethod);
Status status = isGet ? Status.NOT_FOUND : Status.NO_CONTENT;
return Response.status(status).build();
}
try {
if (!(response instanceof WebEndpointResponse)) {
return Response.status(Status.OK).entity(convertIfNecessary(response)).build();
}
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
return Response.status(webEndpointResponse.getStatus())
.header("Content-Type", webEndpointResponse.getContentType())
.entity(convertIfNecessary(webEndpointResponse.getBody())).build();
}
catch (IOException ex) {
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
}
}
private Object convertIfNecessary(Object body) throws IOException {
for (Function<Object, 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<Object, Object> {
@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<Object, Object> {
@Override
public Object apply(Object body) {
if (body instanceof Mono) {
return ((Mono<?>) body).block();
}
return body;
}
}
/**
* Body converter from {@link Flux} to {@link Flux#collectList Mono&lt;List&gt;}.
*/
private static final class FluxBodyConverter implements Function<Object, Object> {
@Override
public Object apply(Object body) {
if (body instanceof Flux) {
return ((Flux<?>) body).collectList();
}
return body;
}
}
/**
* {@link Inflector} to for endpoint links.
*/
private static final class EndpointLinksInflector implements Inflector<ContainerRequestContext, Response> {
private final EndpointLinksResolver linksResolver;
private EndpointLinksInflector(EndpointLinksResolver linksResolver) {
this.linksResolver = linksResolver;
}
@Override
public Response apply(ContainerRequestContext request) {
Map<String, Link> links = this.linksResolver
.resolveLinks(request.getUriInfo().getAbsolutePath().toString());
return Response.ok(Collections.singletonMap("_links", links)).build();
}
}
private static final class JerseySecurityContext implements SecurityContext {
private final jakarta.ws.rs.core.SecurityContext securityContext;
private JerseySecurityContext(jakarta.ws.rs.core.SecurityContext securityContext) {
this.securityContext = securityContext;
}
@Override
public Principal getPrincipal() {
return this.securityContext.getUserPrincipal();
}
@Override
public boolean isUserInRole(String role) {
return this.securityContext.isUserInRole(role);
}
}
}

@ -0,0 +1,66 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.web.jersey;
import java.util.Set;
import org.glassfish.jersey.server.model.Resource;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
/**
* A factory for creating Jersey {@link Resource Resources} for health groups with
* additional path.
*
* @author Madhura Bhave
* @since 2.6.0
*/
public class JerseyHealthEndpointAdditionalPathResourceFactory extends JerseyEndpointResourceFactory {
private final Set<HealthEndpointGroup> groups;
private final WebServerNamespace serverNamespace;
public JerseyHealthEndpointAdditionalPathResourceFactory(WebServerNamespace serverNamespace,
HealthEndpointGroups groups) {
this.serverNamespace = serverNamespace;
this.groups = groups.getAllWithAdditionalPath(serverNamespace);
}
@Override
protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
for (HealthEndpointGroup group : this.groups) {
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
if (additionalPath != null) {
return getResource(endpointMapping, operation, requestPredicate, additionalPath.getValue(),
this.serverNamespace, (data, pathSegmentsVariable) -> data.getUriInfo().getPath());
}
}
}
return null;
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.web.jersey;
import jakarta.ws.rs.container.ContainerRequestContext;
/**
* Strategy interface used to provide the remaining path segments for a Jersey actuator
* endpoint.
*
* @author Madhura Bhave
*/
interface JerseyRemainingPathSegmentProvider {
String get(ContainerRequestContext requestContext, String matchAllRemainingPathSegmentsVariable);
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Jersey support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.web.jersey;

@ -123,7 +123,7 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
}
@Test
void operationWithTrailingSlashShouldNotMatch() {
protected void operationWithTrailingSlashShouldNotMatch() {
load(TestEndpointConfiguration.class,
(client) -> client.get().uri("/test/").exchange().expectStatus().isNotFound());
}

@ -0,0 +1,171 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.web.jersey;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.ext.ContextResolver;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Integration tests for web endpoints exposed using Jersey.
*
* @author Andy Wilkinson
* @see JerseyEndpointResourceFactory
*/
public class JerseyWebEndpointIntegrationTests
extends AbstractWebEndpointIntegrationTests<AnnotationConfigServletWebServerApplicationContext> {
public JerseyWebEndpointIntegrationTests() {
super(JerseyWebEndpointIntegrationTests::createApplicationContext,
JerseyWebEndpointIntegrationTests::applyAuthenticatedConfiguration);
}
private static AnnotationConfigServletWebServerApplicationContext createApplicationContext() {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.register(JerseyConfiguration.class);
return context;
}
private static void applyAuthenticatedConfiguration(AnnotationConfigServletWebServerApplicationContext context) {
context.register(AuthenticatedConfiguration.class);
}
@Override
protected int getPort(AnnotationConfigServletWebServerApplicationContext context) {
return context.getWebServer().getPort();
}
@Override
protected void validateErrorBody(WebTestClient.BodyContentSpec body, HttpStatus status, String path,
String message) {
// Jersey doesn't support the general error page handling
}
@Override
@Test
@Disabled("Jersey does not distinguish between /example and /example/")
protected void operationWithTrailingSlashShouldNotMatch() {
}
@Configuration(proxyBeanMethods = false)
static class JerseyConfiguration {
@Bean
TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(0);
}
@Bean
ServletRegistrationBean<ServletContainer> 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<Resource> 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<ObjectMapper> {
private final ObjectMapper objectMapper;
private ObjectMapperContextResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public ObjectMapper getContext(Class<?> type) {
return this.objectMapper;
}
}
}

@ -17,12 +17,16 @@
package org.springframework.boot.actuate.endpoint.web.test;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.Extension;
@ -37,11 +41,14 @@ import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory;
import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
@ -84,12 +91,22 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext extensionContext) {
return Stream.of(
new WebEndpointsInvocationContext("Jersey",
WebEndpointTestInvocationContextProvider::createJerseyContext),
new WebEndpointsInvocationContext("WebMvc",
WebEndpointTestInvocationContextProvider::createWebMvcContext),
new WebEndpointsInvocationContext("WebFlux",
WebEndpointTestInvocationContextProvider::createWebFluxContext));
}
private static ConfigurableApplicationContext createJerseyContext(List<Class<?>> classes) {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
classes.add(JerseyEndpointConfiguration.class);
context.register(ClassUtils.toClassArray(classes));
context.refresh();
return context;
}
private static ConfigurableApplicationContext createWebMvcContext(List<Class<?>> classes) {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
classes.add(WebMvcEndpointConfiguration.class);
@ -191,6 +208,44 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
}
@Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, JerseyAutoConfiguration.class })
static class JerseyEndpointConfiguration {
private final ApplicationContext applicationContext;
JerseyEndpointConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(0);
}
@Bean
ResourceConfig resourceConfig() {
return new ResourceConfig();
}
@Bean
ResourceConfigCustomizer webEndpointRegistrar() {
return this::customize;
}
private void customize(ResourceConfig config) {
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
Collections.emptyList());
Collection<Resource> resources = new JerseyEndpointResourceFactory().createEndpointResources(
new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes,
new EndpointLinksResolver(discoverer.getEndpoints()), true);
config.registerResources(new HashSet<>(resources));
}
}
@Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, WebFluxAutoConfiguration.class })
static class WebFluxEndpointConfiguration implements ApplicationListener<WebServerInitializedEvent> {

@ -19,6 +19,7 @@ dependencies {
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
optional("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
optional("com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations")
optional("com.fasterxml.jackson.module:jackson-module-parameter-names")
optional("com.google.code.gson:gson")
optional("com.hazelcast:hazelcast")
@ -106,6 +107,11 @@ dependencies {
optional("org.flywaydb:flyway-core")
optional("org.flywaydb:flyway-sqlserver")
optional("org.freemarker:freemarker")
optional("org.glassfish.jersey.containers:jersey-container-servlet-core")
optional("org.glassfish.jersey.containers:jersey-container-servlet")
optional("org.glassfish.jersey.core:jersey-server")
optional("org.glassfish.jersey.ext:jersey-spring6")
optional("org.glassfish.jersey.media:jersey-media-json-jackson")
optional("org.hibernate.orm:hibernate-core")
optional("org.hibernate.orm:hibernate-jcache")
optional("org.hibernate.validator:hibernate-validator")

@ -0,0 +1,232 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.util.Collections;
import java.util.EnumSet;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.xml.bind.annotation.XmlElement;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.spring.SpringComponentProvider;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.servlet.ServletProperties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.DynamicRegistrationBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.util.ClassUtils;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.filter.RequestContextFilter;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Jersey.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Eddú Meléndez
* @author Stephane Nicoll
* @since 1.2.0
*/
@AutoConfiguration(before = DispatcherServletAutoConfiguration.class, after = JacksonAutoConfiguration.class)
@ConditionalOnClass({ SpringComponentProvider.class, ServletRegistration.class })
@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig")
@ConditionalOnWebApplication(type = Type.SERVLET)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@EnableConfigurationProperties(JerseyProperties.class)
public class JerseyAutoConfiguration implements ServletContextAware {
private static final Log logger = LogFactory.getLog(JerseyAutoConfiguration.class);
private final JerseyProperties jersey;
private final ResourceConfig config;
public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config,
ObjectProvider<ResourceConfigCustomizer> customizers) {
this.jersey = jersey;
this.config = config;
customizers.orderedStream().forEach((customizer) -> customizer.customize(this.config));
}
@Bean
@ConditionalOnMissingFilterBean(RequestContextFilter.class)
public FilterRegistrationBean<RequestContextFilter> requestContextFilter() {
FilterRegistrationBean<RequestContextFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestContextFilter());
registration.setOrder(this.jersey.getFilter().getOrder() - 1);
registration.setName("requestContextFilter");
return registration;
}
@Bean
@ConditionalOnMissingBean
public JerseyApplicationPath jerseyApplicationPath() {
return new DefaultJerseyApplicationPath(this.jersey.getApplicationPath(), this.config);
}
@Bean
@ConditionalOnMissingBean(name = "jerseyFilterRegistration")
@ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "filter")
public FilterRegistrationBean<ServletContainer> jerseyFilterRegistration(JerseyApplicationPath applicationPath) {
FilterRegistrationBean<ServletContainer> registration = new FilterRegistrationBean<>();
registration.setFilter(new ServletContainer(this.config));
registration.setUrlPatterns(Collections.singletonList(applicationPath.getUrlMapping()));
registration.setOrder(this.jersey.getFilter().getOrder());
registration.addInitParameter(ServletProperties.FILTER_CONTEXT_PATH, stripPattern(applicationPath.getPath()));
addInitParameters(registration);
registration.setName("jerseyFilter");
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
return registration;
}
private String stripPattern(String path) {
if (path.endsWith("/*")) {
path = path.substring(0, path.lastIndexOf("/*"));
}
return path;
}
@Bean
@ConditionalOnMissingBean(name = "jerseyServletRegistration")
@ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "servlet", matchIfMissing = true)
public ServletRegistrationBean<ServletContainer> jerseyServletRegistration(JerseyApplicationPath applicationPath) {
ServletRegistrationBean<ServletContainer> registration = new ServletRegistrationBean<>(
new ServletContainer(this.config), applicationPath.getUrlMapping());
addInitParameters(registration);
registration.setName(getServletRegistrationName());
registration.setLoadOnStartup(this.jersey.getServlet().getLoadOnStartup());
return registration;
}
private String getServletRegistrationName() {
return ClassUtils.getUserClass(this.config.getClass()).getName();
}
private void addInitParameters(DynamicRegistrationBean<?> registration) {
this.jersey.getInit().forEach(registration::addInitParameter);
}
@Override
public void setServletContext(ServletContext servletContext) {
String servletRegistrationName = getServletRegistrationName();
ServletRegistration registration = servletContext.getServletRegistration(servletRegistrationName);
if (registration != null) {
if (logger.isInfoEnabled()) {
logger.info("Configuring existing registration for Jersey servlet '" + servletRegistrationName + "'");
}
registration.setInitParameters(this.jersey.getInit());
}
}
@Order(Ordered.HIGHEST_PRECEDENCE)
public static final class JerseyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// We need to switch *off* the Jersey WebApplicationInitializer because it
// will try and register a ContextLoaderListener which we don't need
servletContext.setInitParameter("contextConfigLocation", "<NONE>");
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JacksonFeature.class)
@ConditionalOnSingleCandidate(ObjectMapper.class)
static class JacksonResourceConfigCustomizer {
@Bean
ResourceConfigCustomizer resourceConfigCustomizer(final ObjectMapper objectMapper) {
return (ResourceConfig config) -> {
config.register(JacksonFeature.class);
config.register(new ObjectMapperContextResolver(objectMapper), ContextResolver.class);
};
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ JakartaXmlBindAnnotationIntrospector.class, XmlElement.class })
static class JaxbObjectMapperCustomizer {
@Autowired
void addJaxbAnnotationIntrospector(ObjectMapper objectMapper) {
JakartaXmlBindAnnotationIntrospector jaxbAnnotationIntrospector = new JakartaXmlBindAnnotationIntrospector(
objectMapper.getTypeFactory());
objectMapper.setAnnotationIntrospectors(
createPair(objectMapper.getSerializationConfig(), jaxbAnnotationIntrospector),
createPair(objectMapper.getDeserializationConfig(), jaxbAnnotationIntrospector));
}
private AnnotationIntrospector createPair(MapperConfig<?> config,
JakartaXmlBindAnnotationIntrospector jaxbAnnotationIntrospector) {
return AnnotationIntrospector.pair(config.getAnnotationIntrospector(), jaxbAnnotationIntrospector);
}
}
private static final class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
private ObjectMapperContextResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public ObjectMapper getContext(Class<?> type) {
return this.objectMapper;
}
}
}
}

@ -0,0 +1,127 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties @ConfigurationProperties} for Jersey.
*
* @author Dave Syer
* @author Eddú Meléndez
* @author Stephane Nicoll
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "spring.jersey")
public class JerseyProperties {
/**
* Jersey integration type.
*/
private Type type = Type.SERVLET;
/**
* Init parameters to pass to Jersey through the servlet or filter.
*/
private Map<String, String> init = new HashMap<>();
private final Filter filter = new Filter();
private final Servlet servlet = new Servlet();
/**
* Path that serves as the base URI for the application. If specified, overrides the
* value of "@ApplicationPath".
*/
private String applicationPath;
public Filter getFilter() {
return this.filter;
}
public Servlet getServlet() {
return this.servlet;
}
public Type getType() {
return this.type;
}
public void setType(Type type) {
this.type = type;
}
public Map<String, String> getInit() {
return this.init;
}
public void setInit(Map<String, String> init) {
this.init = init;
}
public String getApplicationPath() {
return this.applicationPath;
}
public void setApplicationPath(String applicationPath) {
this.applicationPath = applicationPath;
}
public enum Type {
SERVLET, FILTER
}
public static class Filter {
/**
* Jersey filter chain order.
*/
private int order;
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
}
public static class Servlet {
/**
* Load on startup priority of the Jersey servlet.
*/
private int loadOnStartup = -1;
public int getLoadOnStartup() {
return this.loadOnStartup;
}
public void setLoadOnStartup(int loadOnStartup) {
this.loadOnStartup = loadOnStartup;
}
}
}

@ -0,0 +1,37 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import org.glassfish.jersey.server.ResourceConfig;
/**
* Callback interface that can be implemented by beans wishing to customize Jersey's
* {@link ResourceConfig} before it is used.
*
* @author Eddú Meléndez
* @since 1.4.0
*/
@FunctionalInterface
public interface ResourceConfigCustomizer {
/**
* Customize the resource config.
* @param config the {@link ResourceConfig} to customize
*/
void customize(ResourceConfig config);
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for Jersey.
*/
package org.springframework.boot.autoconfigure.jersey;

@ -0,0 +1,60 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.web.servlet;
import jakarta.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.autoconfigure.jersey.JerseyProperties;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link JerseyApplicationPath} that derives the path from
* {@link JerseyProperties} or the {@code @ApplicationPath} annotation.
*
* @author Madhura Bhave
* @since 2.1.0
*/
public class DefaultJerseyApplicationPath implements JerseyApplicationPath {
private final String applicationPath;
private final ResourceConfig config;
public DefaultJerseyApplicationPath(String applicationPath, ResourceConfig config) {
this.applicationPath = applicationPath;
this.config = config;
}
@Override
public String getPath() {
return resolveApplicationPath();
}
private String resolveApplicationPath() {
if (StringUtils.hasLength(this.applicationPath)) {
return this.applicationPath;
}
// Jersey doesn't like to be the default servlet, so map to /* as a fallback
return MergedAnnotations.from(this.config.getApplication().getClass(), SearchStrategy.TYPE_HIERARCHY)
.get(ApplicationPath.class).getValue(MergedAnnotation.VALUE, String.class).orElse("/*");
}
}

@ -0,0 +1,90 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.web.servlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
/**
* Interface that can be used by auto-configurations that need path details Jersey's
* application path that serves as the base URI for the application.
*
* @author Madhura Bhave
* @since 2.0.7
*/
@FunctionalInterface
public interface JerseyApplicationPath {
/**
* Returns the configured path of the application.
* @return the configured path
*/
String getPath();
/**
* Return a form of the given path that's relative to the Jersey application path.
* @param path the path to make relative
* @return the relative path
*/
default String getRelativePath(String path) {
String prefix = getPrefix();
if (!path.startsWith("/")) {
path = "/" + path;
}
return prefix + path;
}
/**
* Return a cleaned up version of the path that can be used as a prefix for URLs. The
* resulting path will have path will not have a trailing slash.
* @return the prefix
* @see #getRelativePath(String)
*/
default String getPrefix() {
String result = getPath();
int index = result.indexOf('*');
if (index != -1) {
result = result.substring(0, index);
}
if (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
/**
* Return a URL mapping pattern that can be used with a
* {@link ServletRegistrationBean} to map Jersey's servlet.
* @return the path as a servlet URL mapping
*/
default String getUrlMapping() {
String path = getPath();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.equals("/")) {
return "/*";
}
if (path.contains("*")) {
return path;
}
if (path.endsWith("/")) {
return path + "*";
}
return path + "/*";
}
}

@ -1550,6 +1550,10 @@
"level": "error"
}
},
{
"name": "spring.jersey.type",
"defaultValue": "servlet"
},
{
"name": "spring.jpa.hibernate.use-new-id-generator-mappings",
"type": "java.lang.Boolean",

@ -72,6 +72,7 @@ org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration

@ -0,0 +1,98 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Application;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using a custom {@link Application}.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
class JerseyAutoConfigurationCustomApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/test/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@ApplicationPath("/test")
static class TestApplication extends Application {
}
@Path("/hello")
public static class TestController {
@GET
public String message() {
return "Hello World";
}
}
@Configuration(proxyBeanMethods = false)
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
static class TestConfiguration {
@Configuration(proxyBeanMethods = false)
public class JerseyConfiguration {
@Bean
public TestApplication testApplication() {
return new TestApplication();
}
@Bean
public ResourceConfig conf(TestApplication app) {
ResourceConfig config = ResourceConfig.forApplication(app);
config.register(TestController.class);
return config;
}
}
}
}

@ -0,0 +1,100 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom servlet paths.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "spring.jersey.type=filter", "server.servlet.context-path=/app",
"server.servlet.register-default-servlet=true" })
@DirtiesContext
class JerseyAutoConfigurationCustomFilterContextPathTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@MinimalWebConfiguration
@ApplicationPath("/rest")
@Path("/hello")
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,99 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom servlet paths.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" })
@DirtiesContext
class JerseyAutoConfigurationCustomFilterPathTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@MinimalWebConfiguration
@ApplicationPath("rest")
@Path("/hello")
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,76 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom load on startup.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jersey.servlet.load-on-startup=5")
@DirtiesContext
class JerseyAutoConfigurationCustomLoadOnStartupTests {
@Autowired
private ApplicationContext context;
@Test
void contextLoads() {
assertThat(this.context.getBean("jerseyServletRegistration")).hasFieldOrPropertyWithValue("loadOnStartup", 5);
}
@MinimalWebConfiguration
static class Application extends ResourceConfig {
Application() {
register(Application.class);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,126 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom ObjectMapper.
*
* @author Eddú Meléndez
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = "spring.jackson.default-property-inclusion=non_null")
@DirtiesContext
class JerseyAutoConfigurationCustomObjectMapperProviderTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> response = this.restTemplate.getForEntity("/rest/message", String.class);
assertThat(HttpStatus.OK).isEqualTo(response.getStatusCode());
assertThat("{\"subject\":\"Jersey\"}").isEqualTo(response.getBody());
}
@MinimalWebConfiguration
@ApplicationPath("/rest")
@Path("/message")
public static class Application extends ResourceConfig {
Application() {
register(Application.class);
}
@GET
public Message message() {
return new Message("Jersey", null);
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
public static class Message {
private String subject;
private String body;
Message(String subject, String body) {
this.subject = subject;
this.body = body;
}
public String getSubject() {
return this.subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return this.body;
}
public void setBody(String body) {
this.body = body;
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JacksonAutoConfiguration.class,
JerseyAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,98 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom servlet paths.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "server.servlet.contextPath=/app")
@DirtiesContext
class JerseyAutoConfigurationCustomServletContextPathTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@MinimalWebConfiguration
@ApplicationPath("/rest")
@Path("/hello")
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,98 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom servlet paths.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
class JerseyAutoConfigurationCustomServletPathTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/rest/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@MinimalWebConfiguration
@ApplicationPath("/rest")
@Path("/hello")
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,97 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom servlet paths.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" })
@DirtiesContext
class JerseyAutoConfigurationDefaultFilterPathTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@MinimalWebConfiguration
@Path("/hello")
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,96 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using default servlet paths.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
class JerseyAutoConfigurationDefaultServletPathTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@MinimalWebConfiguration
@Path("/hello")
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,136 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.xml.bind.annotation.XmlTransient;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} with an ObjectMapper.
*
* @author Eddú Meléndez
* @author Andy Wilkinson
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = "spring.jackson.default-property-inclusion:non-null")
@DirtiesContext
class JerseyAutoConfigurationObjectMapperProviderTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void responseIsSerializedUsingAutoConfiguredObjectMapper() {
ResponseEntity<String> response = this.restTemplate.getForEntity("/rest/message", String.class);
assertThat(HttpStatus.OK).isEqualTo(response.getStatusCode());
assertThat(response.getBody()).isEqualTo("{\"subject\":\"Jersey\"}");
}
@MinimalWebConfiguration
@ApplicationPath("/rest")
@Path("/message")
public static class Application extends ResourceConfig {
Application() {
register(Application.class);
}
@GET
public Message message() {
return new Message("Jersey", null);
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
public static class Message {
private String subject;
private String body;
Message() {
}
Message(String subject, String body) {
this.subject = subject;
this.body = body;
}
public String getSubject() {
return this.subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return this.body;
}
public void setBody(String body) {
this.body = body;
}
@XmlTransient
public String getFoo() {
return "foo";
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JacksonAutoConfiguration.class,
JerseyAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,111 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.nio.charset.StandardCharsets;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.tomcat.util.buf.UDecoder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfigurationServletContainerTests.Application;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests that verify the behavior when deployed to a Servlet container where Jersey may
* have already initialized itself.
*
* @author Andy Wilkinson
*/
@SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
@ExtendWith(OutputCaptureExtension.class)
class JerseyAutoConfigurationServletContainerTests {
@Test
void existingJerseyServletIsAmended(CapturedOutput output) {
assertThat(output).contains("Configuring existing registration for Jersey servlet");
assertThat(output).contains("Servlet " + Application.class.getName() + " was not registered");
}
@ImportAutoConfiguration({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
@Import(ContainerConfiguration.class)
@Path("/hello")
@Configuration(proxyBeanMethods = false)
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
}
@Configuration(proxyBeanMethods = false)
static class ContainerConfiguration {
@Bean
TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
Wrapper jerseyServlet = context.createWrapper();
String servletName = Application.class.getName();
jerseyServlet.setName(servletName);
jerseyServlet.setServletClass(ServletContainer.class.getName());
jerseyServlet.setServlet(new ServletContainer());
jerseyServlet.setOverridable(false);
context.addChild(jerseyServlet);
String pattern = UDecoder.URLDecode("/*", StandardCharsets.UTF_8);
context.addServletMappingDecoded(pattern, servletName);
}
};
}
}
}

@ -0,0 +1,130 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.RequestContextFilter;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration}.
*
* @author Andy Wilkinson
*/
class JerseyAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class))
.withUserConfiguration(ResourceConfigConfiguration.class);
@Test
void requestContextFilterRegistrationIsAutoConfigured() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
FilterRegistrationBean<?> registration = context.getBean(FilterRegistrationBean.class);
assertThat(registration.getFilter()).isInstanceOf(RequestContextFilter.class);
});
}
@Test
void whenUserDefinesARequestContextFilterTheAutoConfiguredRegistrationBacksOff() {
this.contextRunner.withUserConfiguration(RequestContextFilterConfiguration.class).run((context) -> {
assertThat(context).doesNotHaveBean(FilterRegistrationBean.class);
assertThat(context).hasSingleBean(RequestContextFilter.class);
});
}
@Test
void whenUserDefinesARequestContextFilterRegistrationTheAutoConfiguredRegistrationBacksOff() {
this.contextRunner.withUserConfiguration(RequestContextFilterRegistrationConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
assertThat(context).hasBean("customRequestContextFilterRegistration");
});
}
@Test
void whenJaxbIsAvailableTheObjectMapperIsCustomizedWithAnAnnotationIntrospector() {
this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
assertThat(objectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors().stream()
.filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).hasSize(1);
});
}
@Test
void whenJaxbIsNotAvailableTheObjectMapperCustomizationBacksOff() {
this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader("jakarta.xml.bind.annotation")).run((context) -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
assertThat(objectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors()
.stream().filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).isEmpty();
});
}
@Test
void whenJacksonJaxbModuleIsNotAvailableTheObjectMapperCustomizationBacksOff() {
this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(JakartaXmlBindAnnotationIntrospector.class)).run((context) -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
assertThat(objectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors()
.stream().filter(JakartaXmlBindAnnotationIntrospector.class::isInstance)).isEmpty();
});
}
@Configuration(proxyBeanMethods = false)
static class ResourceConfigConfiguration {
@Bean
ResourceConfig resourceConfig() {
return new ResourceConfig();
}
}
@Configuration(proxyBeanMethods = false)
static class RequestContextFilterConfiguration {
@Bean
RequestContextFilter requestContextFilter() {
return new RequestContextFilter();
}
}
@Configuration(proxyBeanMethods = false)
static class RequestContextFilterRegistrationConfiguration {
@Bean
FilterRegistrationBean<RequestContextFilter> customRequestContextFilterRegistration() {
return new FilterRegistrationBean<>(new RequestContextFilter());
}
}
}

@ -0,0 +1,96 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.jersey;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyAutoConfiguration} when using custom application path.
*
* @author Eddú Meléndez
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jersey.application-path=/api")
@DirtiesContext
class JerseyAutoConfigurationWithoutApplicationPathTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/api/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@MinimalWebConfiguration
@Path("/hello")
public static class Application extends ResourceConfig {
@Value("${message:World}")
private String msg;
Application() {
register(Application.class);
}
@GET
public String message() {
return "Hello " + this.msg;
}
static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({ ServletWebServerFactoryAutoConfiguration.class, JerseyAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
}

@ -0,0 +1,83 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.web.servlet;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JerseyApplicationPath}.
*
* @author Madhura Bhave
*/
class JerseyApplicationPathTests {
@Test
void getRelativePathReturnsRelativePath() {
assertThat(((JerseyApplicationPath) () -> "spring").getRelativePath("boot")).isEqualTo("spring/boot");
assertThat(((JerseyApplicationPath) () -> "spring/").getRelativePath("boot")).isEqualTo("spring/boot");
assertThat(((JerseyApplicationPath) () -> "spring").getRelativePath("/boot")).isEqualTo("spring/boot");
assertThat(((JerseyApplicationPath) () -> "spring/*").getRelativePath("/boot")).isEqualTo("spring/boot");
}
@Test
void getPrefixWhenHasSimplePathReturnPath() {
assertThat(((JerseyApplicationPath) () -> "spring").getPrefix()).isEqualTo("spring");
}
@Test
void getPrefixWhenHasPatternRemovesPattern() {
assertThat(((JerseyApplicationPath) () -> "spring/*.do").getPrefix()).isEqualTo("spring");
}
@Test
void getPrefixWhenPathEndsWithSlashRemovesSlash() {
assertThat(((JerseyApplicationPath) () -> "spring/").getPrefix()).isEqualTo("spring");
}
@Test
void getUrlMappingWhenPathIsEmptyReturnsSlash() {
assertThat(((JerseyApplicationPath) () -> "").getUrlMapping()).isEqualTo("/*");
}
@Test
void getUrlMappingWhenPathIsSlashReturnsSlash() {
assertThat(((JerseyApplicationPath) () -> "/").getUrlMapping()).isEqualTo("/*");
}
@Test
void getUrlMappingWhenPathContainsStarReturnsPath() {
assertThat(((JerseyApplicationPath) () -> "/spring/*.do").getUrlMapping()).isEqualTo("/spring/*.do");
}
@Test
void getUrlMappingWhenHasPathNotEndingSlashReturnsSlashStarPattern() {
assertThat(((JerseyApplicationPath) () -> "/spring/boot").getUrlMapping()).isEqualTo("/spring/boot/*");
}
@Test
void getUrlMappingWhenHasPathDoesNotStartWithSlashPrependsSlash() {
assertThat(((JerseyApplicationPath) () -> "spring/boot").getUrlMapping()).isEqualTo("/spring/boot/*");
}
@Test
void getUrlMappingWhenHasPathEndingWithSlashReturnsSlashStarPattern() {
assertThat(((JerseyApplicationPath) () -> "/spring/boot/").getUrlMapping()).isEqualTo("/spring/boot/*");
}
}

@ -692,6 +692,13 @@ bom {
]
}
}
library("Jersey", "3.0.6") {
group("org.glassfish.jersey") {
imports = [
"jersey-bom"
]
}
}
library("Jetty EL", "10.0.14") {
group("org.mortbay.jasper") {
modules = [
@ -1293,6 +1300,7 @@ bom {
"spring-boot-starter-hateoas",
"spring-boot-starter-integration",
"spring-boot-starter-jdbc",
"spring-boot-starter-jersey",
"spring-boot-starter-jetty",
"spring-boot-starter-jooq",
"spring-boot-starter-json",

@ -96,6 +96,8 @@ dependencies {
implementation("org.assertj:assertj-core")
implementation("org.cache2k:cache2k-spring")
implementation("org.apache.groovy:groovy")
implementation("org.glassfish.jersey.containers:jersey-container-servlet-core")
implementation("org.glassfish.jersey.core:jersey-server")
implementation("org.hibernate.orm:hibernate-jcache") {
exclude group: "javax.activation", module: "javax.activation-api"
exclude group: "javax.persistence", module: "javax.persistence-api"

@ -90,7 +90,7 @@ The following technology-agnostic endpoints are available:
| Performs a thread dump.
|===
If your application is a web application (Spring MVC or Spring WebFlux), you can use the following additional endpoints:
If your application is a web application (Spring MVC, Spring WebFlux, or Jersey), you can use the following additional endpoints:
[cols="2,5"]
|===
@ -424,7 +424,8 @@ TIP: See {spring-boot-actuator-autoconfigure-module-code}/endpoint/web/CorsEndpo
[[actuator.endpoints.implementing-custom]]
=== Implementing Custom Endpoints
If you add a `@Bean` annotated with `@Endpoint`, any methods annotated with `@ReadOperation`, `@WriteOperation`, or `@DeleteOperation` are automatically exposed over JMX and, in a web application, over HTTP as well.
Endpoints can be exposed over HTTP by using Spring MVC, or Spring WebFlux.
Endpoints can be exposed over HTTP by using Jersey, Spring MVC, or Spring WebFlux.
If both Jersey and Spring MVC are available, Spring MVC is used.
The following example exposes a read operation that returns a custom object:
@ -481,7 +482,8 @@ Before calling an operation method, the input received over JMX or HTTP is conve
[[actuator.endpoints.implementing-custom.web]]
==== Custom Web Endpoints
Operations on an `@Endpoint`, `@WebEndpoint`, or `@EndpointWebExtension` are automatically exposed over HTTP using Spring MVC or Spring WebFlux.
Operations on an `@Endpoint`, `@WebEndpoint`, or `@EndpointWebExtension` are automatically exposed over HTTP using Jersey, Spring MVC, or Spring WebFlux.
If both Jersey and Spring MVC are available, Spring MVC is used.
@ -560,7 +562,9 @@ If an operation is invoked without a required parameter or with a parameter that
[[actuator.endpoints.implementing-custom.web.range-requests]]
===== Web Endpoint Range Requests
You can use an HTTP range request to request part of an HTTP resource.
Operations that return a `org.springframework.core.io.Resource` automatically support range requests.
When using Spring MVC or Spring Web Flux, operations that return a `org.springframework.core.io.Resource` automatically support range requests.
NOTE: Range requests are not supported when using Jersey.

@ -819,6 +819,41 @@ Applications can opt in and record exceptions by <<web#web.reactive.webflux.erro
[[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.metrics.web.server.request.metric-name[] property.
`@Timed` annotations are supported on request-handling classes and methods (see <<actuator#actuator.metrics.supported.timed-annotation>> for details).
If you do not want to record metrics for all Jersey requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead.
By default, Jersey server metrics are tagged with the following information:
|===
| Tag | Description
| `exception`
| The simple class name of any exception that was thrown while handling the request.
| `method`
| The request's method (for example, `GET` or `POST`)
| `outcome`
| The request's outcome, based on the status code of the response.
1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx is `CLIENT_ERROR`, and 5xx is `SERVER_ERROR`
| `status`
| The response's HTTP status code (for example, `200` or `500`)
| `uri`
| The request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`)
|===
To customize the tags, provide a `@Bean` that implements `JerseyTagsProvider`.
[[actuator.metrics.supported.http-clients]]
==== HTTP Client Metrics
Spring Boot Actuator manages the instrumentation of both `RestTemplate` and `WebClient`.

@ -4,7 +4,8 @@ If you are developing a web application, Spring Boot Actuator auto-configures al
The default convention is to use the `id` of the endpoint with a prefix of `/actuator` as the URL path.
For example, `health` is exposed as `/actuator/health`.
TIP: Actuator is supported natively with Spring MVC and Spring WebFlux.
TIP: Actuator is supported natively with Spring MVC, Spring WebFlux, and Jersey.
If both Jersey and Spring MVC are available, Spring MVC is used.
NOTE: Jackson is a required dependency in order to get the correct JSON responses as documented in the API documentation ({spring-boot-actuator-restapi-docs}[HTML] or {spring-boot-actuator-restapi-pdfdocs}[PDF]).

@ -186,6 +186,7 @@ boot-features-webflux-template-engines=features.developing-web-applications.spri
boot-features-webflux-error-handling=features.developing-web-applications.spring-webflux.error-handling
boot-features-webflux-error-handling-custom-error-pages=features.developing-web-applications.spring-webflux.error-handling.error-pages
boot-features-webflux-web-filters=features.developing-web-applications.spring-webflux.web-filters
boot-features-jersey=features.developing-web-applications.jersey
boot-features-embedded-container=features.developing-web-applications.embedded-container
boot-features-embedded-container-servlets-filters-listeners=features.developing-web-applications.embedded-container.servlets-filters-listeners
boot-features-embedded-container-servlets-filters-listeners-beans=features.developing-web-applications.embedded-container.servlets-filters-listeners.beans
@ -472,6 +473,7 @@ production-ready-metrics-system=actuator.metrics.supported.system
production-ready-metrics-logger=actuator.metrics.supported.logger
production-ready-metrics-spring-mvc=actuator.metrics.supported.spring-mvc
production-ready-metrics-web-flux=actuator.metrics.supported.spring-webflux
production-ready-metrics-jersey-server=actuator.metrics.supported.jersey
production-ready-metrics-http-clients=actuator.metrics.supported.http-clients
production-ready-metrics-tomcat=actuator.metrics.supported.tomcat
production-ready-metrics-cache=actuator.metrics.supported.cache
@ -615,6 +617,9 @@ howto-switch-off-the-spring-mvc-dispatcherservlet=howto.spring-mvc.switch-off-di
howto-switch-off-default-mvc-configuration=howto.spring-mvc.switch-off-default-configuration
howto-customize-view-resolvers=howto.spring-mvc.customize-view-resolvers
howto-use-test-with-spring-security=howto.spring-mvc.testing.with-spring-security
howto-jersey=howto.jersey
howto-jersey-spring-security=howto.jersey.spring-security
howto-jersey-alongside-another-web-framework=howto.jersey.alongside-another-web-framework
howto-http-clients=howto.http-clients
howto-http-clients-proxy-configuration=howto.http-clients.rest-template-proxy-configuration
howto-webclient-reactor-netty-customization=howto.http-clients.webclient-reactor-netty-customization
@ -768,6 +773,7 @@ features.developing-web-applications.spring-mvc.error-handling.error-pages=web.s
features.developing-web-applications.spring-mvc.error-handling.error-pages-without-spring-mvc=web.servlet.spring-mvc.error-handling.error-pages-without-spring-mvc
features.developing-web-applications.spring-mvc.error-handling.in-a-war-deployment=web.servlet.spring-mvc.error-handling.in-a-war-deployment
features.developing-web-applications.spring-mvc.cors=web.servlet.spring-mvc.cors
features.developing-web-applications.jersey=web.servlet.jersey
features.developing-web-applications.embedded-container=web.servlet.embedded-container
features.developing-web-applications.embedded-container.servlets-filters-listeners=web.servlet.embedded-container.servlets-filters-listeners
features.developing-web-applications.embedded-container.servlets-filters-listeners.beans=web.servlet.embedded-container.servlets-filters-listeners.beans

@ -2,7 +2,7 @@
== Web
If you develop Spring Boot web applications, take a look at the following content:
* *Servlet Web Applications:* <<web#web.servlet, Spring MVC, Embedded Servlet Containers>>
* *Servlet Web Applications:* <<web#web.servlet, Spring MVC, Jersey, Embedded Servlet Containers>>
* *Reactive Web Applications:* <<web#web.reactive, Spring Webflux, Embedded Servlet Containers>>
* *Graceful Shutdown:* <<web#web.graceful-shutdown, Graceful Shutdown>>
* *Spring Security:* <<web#web.security, Default Security Configuration, Auto-configuration for OAuth2, SAML>>

@ -23,6 +23,8 @@ include::howto/webserver.adoc[]
include::howto/spring-mvc.adoc[]
include::howto/jersey.adoc[]
include::howto/http-clients.adoc[]
include::howto/logging.adoc[]

@ -0,0 +1,30 @@
[[howto.jersey]]
== Jersey
[[howto.jersey.spring-security]]
=== Secure Jersey endpoints with Spring Security
Spring Security can be used to secure a Jersey-based web application in much the same way as it can be used to secure a Spring MVC-based web application.
However, if you want to use Spring Security's method-level security with Jersey, you must configure Jersey to use `setStatus(int)` rather `sendError(int)`.
This prevents Jersey from committing the response before Spring Security has had an opportunity to report an authentication or authorization failure to the client.
The `jersey.config.server.response.setStatusOverSendError` property must be set to `true` on the application's `ResourceConfig` bean, as shown in the following example:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java[]
----
[[howto.jersey.alongside-another-web-framework]]
=== Use Jersey Alongside Another Web Framework
To use Jersey alongside another web framework, such as Spring MVC, it should be configured so that it will allow the other framework to handle requests that it cannot handle.
First, configure Jersey to use a filter rather than a servlet by configuring the configprop:spring.jersey.type[] application property with a value of `filter`.
Second, configure your `ResourceConfig` to forward requests that would have resulted in a 404, as shown in the following example.
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/howto/jersey/alongsideanotherwebframework/JerseyConfig.java[]
----

@ -1,6 +1,6 @@
[[web.servlet]]
== Servlet Web Applications
If you want to build servlet-based web applications, you can take advantage of Spring Boot's auto-configuration for Spring MVC.
If you want to build servlet-based web applications, you can take advantage of Spring Boot's auto-configuration for Spring MVC or Jersey.
@ -422,6 +422,48 @@ include::code:MyCorsConfiguration[]
[[web.servlet.jersey]]
=== JAX-RS and Jersey
If you prefer the JAX-RS programming model for REST endpoints, you can use one of the available implementations instead of Spring MVC.
https://jersey.github.io/[Jersey] and https://cxf.apache.org/[Apache CXF] work quite well out of the box.
CXF requires you to register its `Servlet` or `Filter` as a `@Bean` in your application context.
Jersey has some native Spring support, so we also provide auto-configuration support for it in Spring Boot, together with a starter.
To get started with Jersey, include the `spring-boot-starter-jersey` as a dependency and then you need one `@Bean` of type `ResourceConfig` in which you register all the endpoints, as shown in the following example:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/web/servlet/jersey/MyJerseyConfig.java[]
----
WARNING: Jersey's support for scanning executable archives is rather limited.
For example, it cannot scan for endpoints in a package found in a <<deployment#deployment.installing, 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 `ResourceConfigCustomizer`.
All the registered endpoints should be `@Components` with HTTP resource annotations (`@GET` and others), as shown in the following example:
[source,java,indent=0,subs="verbatim"]
----
include::{docs-java}/web/servlet/jersey/MyEndpoint.java[]
----
Since the `Endpoint` is a Spring `@Component`, its lifecycle is managed by Spring and you can use the `@Autowired` annotation to inject dependencies and use the `@Value` annotation to inject external configuration.
By default, the Jersey servlet is registered and mapped to `/*`.
You can change the mapping by adding `@ApplicationPath` to your `ResourceConfig`.
By default, Jersey is set up as a servlet in a `@Bean` of type `ServletRegistrationBean` named `jerseyServletRegistration`.
By default, the servlet is initialized lazily, but you can customize that behavior by setting `spring.jersey.servlet.load-on-startup`.
You can disable or override that bean by creating one of your own with the same name.
You can also use a filter instead of a servlet by setting `spring.jersey.type=filter` (in which case, the `@Bean` to replace or override is `jerseyFilterRegistration`).
The filter has an `@Order`, which you can set with `spring.jersey.filter.order`.
When using Jersey as a filter, a servlet that will handle any requests that are not intercepted by Jersey must be present.
If your application does not contain such a servlet, you may want to enable the default servlet by setting configprop:server.servlet.register-default-servlet[] to `true`.
Both the servlet and the filter registrations can be given init parameters by using `spring.jersey.init.*` to specify a map of properties.
[[web.servlet.embedded-container]]
=== Embedded Servlet Container Support
For servlet application, Spring Boot includes support for embedded https://tomcat.apache.org/[Tomcat], https://www.eclipse.org/jetty/[Jetty], and https://github.com/undertow-io/undertow[Undertow] servers.

@ -0,0 +1,21 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.howto.jersey.alongsideanotherwebframework;
class Endpoint {
}

@ -0,0 +1,32 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.howto.jersey.alongsideanotherwebframework;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletProperties;
import org.springframework.stereotype.Component;
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
register(Endpoint.class);
property(ServletProperties.FILTER_FORWARD_ON_404, true);
}
}

@ -0,0 +1,21 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.howto.jersey.springsecurity;
class Endpoint {
}

@ -0,0 +1,33 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.howto.jersey.springsecurity;
import java.util.Collections;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@Component
public class JerseySetStatusOverSendErrorConfig extends ResourceConfig {
public JerseySetStatusOverSendErrorConfig() {
register(Endpoint.class);
setProperties(Collections.singletonMap("jersey.config.server.response.setStatusOverSendError", true));
}
}

@ -0,0 +1,33 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.web.servlet.jersey;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.springframework.stereotype.Component;
@Component
@Path("/hello")
public class MyEndpoint {
@GET
public String message() {
return "Hello";
}
}

@ -0,0 +1,30 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.web.servlet.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@Component
public class MyJerseyConfig extends ResourceConfig {
public MyJerseyConfig() {
register(MyEndpoint.class);
}
}

@ -0,0 +1,26 @@
plugins {
id "org.springframework.boot.starter"
}
description = "Starter for building RESTful web applications using JAX-RS and Jersey. An alternative to spring-boot-starter-web"
dependencies {
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json"))
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-tomcat"))
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-validation"))
api("org.springframework:spring-web")
api("org.glassfish.jersey.containers:jersey-container-servlet-core")
api("org.glassfish.jersey.containers:jersey-container-servlet")
api("org.glassfish.jersey.core:jersey-server")
api("org.glassfish.jersey.ext:jersey-bean-validation") {
exclude group: "jakarta.el", module: "jakarta.el-api"
exclude group: "org.glassfish", module: "jakarta.el"
}
api("org.glassfish.jersey.ext:jersey-spring6")
api("org.glassfish.jersey.media:jersey-media-json-jackson")
}
checkRuntimeClasspathForConflicts {
ignore { name -> name.startsWith("org/aopalliance/intercept/") }
ignore { name -> name.startsWith("org/aopalliance/aop/") }
}

@ -90,6 +90,8 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr
private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework.web.servlet.DispatcherServlet";
private static final String JERSEY_WEB_ENVIRONMENT_CLASS = "org.glassfish.jersey.server.ResourceConfig";
private static final String ACTIVATE_SERVLET_LISTENER = "org.springframework.test."
+ "context.web.ServletTestExecutionListener.activateListener";
@ -175,7 +177,8 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr
private WebApplicationType deduceWebApplicationType() {
if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
&& !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {
&& !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_WEB_ENVIRONMENT_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : WEB_ENVIRONMENT_CLASSES) {

@ -55,9 +55,11 @@ public enum WebApplicationType {
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)) {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
@ -75,6 +77,7 @@ public enum WebApplicationType {
for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) {
registerTypeIfPresent(servletIndicatorClass, classLoader, hints);
}
registerTypeIfPresent(JERSEY_INDICATOR_CLASS, classLoader, hints);
registerTypeIfPresent(WEBFLUX_INDICATOR_CLASS, classLoader, hints);
registerTypeIfPresent(WEBMVC_INDICATOR_CLASS, classLoader, hints);
}

@ -0,0 +1,18 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot Jersey smoke test"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jersey"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-tomcat"))
if (JavaVersion.current().java9Compatible) {
runtimeOnly("jakarta.xml.bind:jakarta.xml.bind-api")
}
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
}

@ -0,0 +1,39 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.springframework.stereotype.Component;
@Component
@Path("/hello")
public class Endpoint {
private final Service service;
public Endpoint(Service service) {
this.service = service;
}
@GET
public String message() {
return "Hello " + this.service.message();
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
register(Endpoint.class);
register(ReverseEndpoint.class);
}
}

@ -0,0 +1,35 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import org.springframework.stereotype.Component;
@Component
@Path("/reverse")
public class ReverseEndpoint {
@GET
public String reverse(@QueryParam("input") @NotNull String input) {
return new StringBuilder(input).reverse().toString();
}
}

@ -0,0 +1,30 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class SampleJerseyApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
new SampleJerseyApplication().configure(new SpringApplicationBuilder(SampleJerseyApplication.class)).run(args);
}
}

@ -0,0 +1,32 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Service {
@Value("${message:World}")
private String msg;
public String message() {
return this.msg;
}
}

@ -0,0 +1,62 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "logging.level.root=debug")
abstract class AbstractJerseyApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/hello", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void reverse() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/reverse?input=olleh", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("hello");
}
@Test
void validation() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/reverse", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
void actuatorStatus() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\"}");
}
}

@ -0,0 +1,112 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import smoketest.jersey.AbstractJerseyManagementPortTests.ResourceConfigConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base class for integration tests for Jersey using separate management and main service
* ports.
*
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "management.server.port=0")
@Import(ResourceConfigConfiguration.class)
class AbstractJerseyManagementPortTests {
@LocalServerPort
private int port;
@LocalManagementPort
private int managementPort;
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void resourceShouldBeAvailableOnMainPort() {
ResponseEntity<String> 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<String> entity = this.testRestTemplate
.getForEntity("http://localhost:" + this.managementPort + "/test", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void actuatorShouldBeAvailableOnManagementPort() {
ResponseEntity<String> entity = this.testRestTemplate
.getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void actuatorShouldNotBeAvailableOnMainPort() {
ResponseEntity<String> entity = this.testRestTemplate
.getForEntity("http://localhost:" + this.port + "/actuator/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@TestConfiguration
static class ResourceConfigConfiguration {
@Bean
ResourceConfigCustomizer customizer() {
return new ResourceConfigCustomizer() {
@Override
public void customize(ResourceConfig config) {
config.register(TestEndpoint.class);
}
};
}
@Path("/test")
public static class TestEndpoint {
@GET
public String test() {
return "test";
}
}
}
}

@ -0,0 +1,58 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for separate management and main service ports with custom
* application path.
*
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "management.server.port=0", "spring.jersey.application-path=/app" })
class JerseyApplicationPathAndManagementPortTests {
@LocalServerPort
private int port;
@LocalManagementPort
private int managementPort;
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void applicationPathShouldNotAffectActuators() {
ResponseEntity<String> entity = this.testRestTemplate
.getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("\"status\":\"UP\"");
}
}

@ -0,0 +1,54 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for separate management and main service ports with empty base path
* for endpoints.
*
* @author HaiTao Zhang
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "management.server.port=0", "management.endpoints.web.base-path=/" })
class JerseyDifferentPortSampleActuatorApplicationTests {
@LocalManagementPort
private int managementPort;
@Test
void linksEndpointShouldBeAvailable() {
ResponseEntity<String> 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";
}
}

@ -0,0 +1,29 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.springframework.test.context.TestPropertySource;
/**
* Smoke tests for Jersey configured as a Filter.
*
* @author Andy Wilkinson
*/
@TestPropertySource(properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" })
class JerseyFilterApplicationTests extends AbstractJerseyApplicationTests {
}

@ -0,0 +1,30 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
import org.springframework.test.context.TestPropertySource;
/**
* Integration tests for Jersey configured as a Servlet using separate management and main
* service ports.
*
* @author Andy Wilkinson
*/
@TestPropertySource(properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" })
class JerseyFilterManagementPortTests extends AbstractJerseyManagementPortTests {
}

@ -0,0 +1,26 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
/**
* Smoke tests for Jersey configured as a Servlet.
*
* @author Andy Wilkinson
*/
class JerseyServletApplicationTests extends AbstractJerseyApplicationTests {
}

@ -0,0 +1,27 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.jersey;
/**
* Integration tests for Jersey configured as a Servlet using separate management and main
* service ports.
*
* @author Andy Wilkinson
*/
class JerseyServletManagementPortTests extends AbstractJerseyManagementPortTests {
}

@ -0,0 +1,14 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot secure Jersey smoke test"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jersey"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-security"))
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
}

@ -0,0 +1,39 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.secure.jersey;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.springframework.stereotype.Component;
@Component
@Path("/hello")
public class Endpoint {
private final Service service;
public Endpoint(Service service) {
this.service = service;
}
@GET
public String message() {
return "Hello " + this.service.message();
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.secure.jersey;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
register(Endpoint.class);
register(ReverseEndpoint.class);
}
}

@ -0,0 +1,35 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.secure.jersey;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import org.springframework.stereotype.Component;
@Component
@Path("/reverse")
public class ReverseEndpoint {
@GET
public String reverse(@QueryParam("input") @NotNull String input) {
return new StringBuilder(input).reverse().toString();
}
}

@ -0,0 +1,63 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.secure.jersey;
import java.io.IOException;
import java.util.function.Supplier;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.endpoint.web.EndpointServlet;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SampleSecureJerseyApplication {
public static void main(String[] args) {
SpringApplication.run(SampleSecureJerseyApplication.class, args);
}
@Bean
TestServletEndpoint servletEndpoint() {
return new TestServletEndpoint();
}
@ServletEndpoint(id = "se1")
static class TestServletEndpoint implements Supplier<EndpointServlet> {
@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 {
}
}
}

@ -0,0 +1,54 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.secure.jersey;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.actuate.web.mappings.MappingsEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@SuppressWarnings("deprecation")
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
return new InMemoryUserDetailsManager(
User.withDefaultPasswordEncoder().username("user").password("password").authorities("ROLE_USER")
.build(),
User.withDefaultPasswordEncoder().username("admin").password("admin")
.authorities("ROLE_ACTUATOR", "ROLE_USER").build());
}
@Bean
SecurityFilterChain configure(HttpSecurity http) throws Exception {
// @formatter:off
http.authorizeRequests()
.requestMatchers(EndpointRequest.to("health")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR")
.antMatchers("/**").hasRole("USER")
.and()
.httpBasic();
return http.build();
// @formatter:on
}
}

@ -0,0 +1,32 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.secure.jersey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Service {
@Value("${message:World}")
private String msg;
public String message() {
return this.msg;
}
}

@ -0,0 +1,4 @@
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save