Skip to content

Commit b8d9b25

Browse files
jmartiskgsmet
authored andcommitted
Filter out disabled REST methods from the OpenAPI document
(cherry picked from commit 1e779c2)
1 parent 4278d7a commit b8d9b25

File tree

10 files changed

+215
-19
lines changed

10 files changed

+215
-19
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.quarkus.runtime.rest;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
/**
7+
* This class serves for passing a list of disabled REST paths (via the `@EndpointDisabled` annotation)
8+
* so that an OpenAPI filter can omit them from the generated OpenAPI document.
9+
*/
10+
public class DisabledRestEndpoints {
11+
12+
// keys are REST paths, values are HTTP methods disabled on the given path
13+
private static Map<String, List<String>> endpoints;
14+
15+
public static void set(Map<String, List<String>> endpoints) {
16+
DisabledRestEndpoints.endpoints = endpoints;
17+
}
18+
19+
public static Map<String, List<String>> get() {
20+
return endpoints;
21+
}
22+
}

extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import io.quarkus.runtime.RuntimeValue;
5454
import io.quarkus.runtime.ShutdownContext;
5555
import io.quarkus.runtime.annotations.Recorder;
56+
import io.quarkus.runtime.rest.DisabledRestEndpoints;
5657
import io.quarkus.security.AuthenticationCompletionException;
5758
import io.quarkus.security.AuthenticationException;
5859
import io.quarkus.security.AuthenticationFailedException;
@@ -182,6 +183,7 @@ public ResteasyReactiveRequestContext createContext(Deployment deployment,
182183
closeTaskHandler, contextFactory, new ArcThreadSetupAction(beanContainer.requestContext()),
183184
vertxConfig.rootPath);
184185
Deployment deployment = runtimeDeploymentManager.deploy();
186+
DisabledRestEndpoints.set(deployment.getDisabledEndpoints());
185187
initClassFactory.createInstance().getInstance().init(deployment);
186188
currentDeployment = deployment;
187189

extensions/smallrye-openapi/deployment/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
<!-- Test -->
7070
<dependency>
7171
<groupId>io.quarkus</groupId>
72-
<artifactId>quarkus-resteasy-deployment</artifactId>
72+
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
7373
<scope>test</scope>
7474
</dependency>
7575
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import static org.hamcrest.Matchers.notNullValue;
4+
import static org.hamcrest.Matchers.nullValue;
5+
6+
import jakarta.ws.rs.GET;
7+
import jakarta.ws.rs.POST;
8+
import jakarta.ws.rs.PUT;
9+
import jakarta.ws.rs.Path;
10+
import jakarta.ws.rs.QueryParam;
11+
12+
import org.jboss.shrinkwrap.api.asset.StringAsset;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.RegisterExtension;
15+
16+
import io.quarkus.resteasy.reactive.server.EndpointDisabled;
17+
import io.quarkus.test.QuarkusUnitTest;
18+
import io.restassured.RestAssured;
19+
20+
/**
21+
* Verify that REST endpoints that are disabled via the {@link EndpointDisabled} annotation are not included in the OpenAPI
22+
* document.
23+
*/
24+
public class DisabledEndpointTestCase {
25+
@RegisterExtension
26+
static QuarkusUnitTest runner = new QuarkusUnitTest()
27+
.withApplicationRoot((jar) -> jar
28+
.addClasses(DisabledEndpoint.class,
29+
EnabledEndpoint.class)
30+
.add(new StringAsset("quarkus.http.root-path=/root\n"), "application.properties"));
31+
32+
@EndpointDisabled(name = "xxx", disableIfMissing = true, stringValue = "xxx")
33+
@Path("/disabled")
34+
public static class DisabledEndpoint {
35+
36+
@Path("/hello")
37+
@GET
38+
public String hello() {
39+
return null;
40+
}
41+
42+
@Path("/hello2/{param1}")
43+
@GET
44+
public String hello2(@QueryParam("param1") String param1) {
45+
return null;
46+
}
47+
48+
@Path("/hello5")
49+
@PUT
50+
public String hello5() {
51+
return null;
52+
}
53+
54+
}
55+
56+
@EndpointDisabled(name = "xxx", disableIfMissing = false, stringValue = "xxx")
57+
@Path("/enabled")
58+
public static class EnabledEndpoint {
59+
60+
@Path("/hello3")
61+
@GET
62+
public String hello() {
63+
return null;
64+
}
65+
66+
@Path("/hello4/{param1}")
67+
@GET
68+
public String hello4(@QueryParam("param1") String param1) {
69+
return null;
70+
}
71+
72+
@Path("/hello5")
73+
@POST
74+
public String hello5() {
75+
return null;
76+
}
77+
78+
}
79+
80+
@Test
81+
public void testDisabledEndpoint() {
82+
RestAssured.given().header("Accept", "application/json")
83+
.when().get("/q/openapi")
84+
.prettyPeek().then()
85+
.body("paths.\"/root/disabled/hello\".get", nullValue())
86+
.body("paths.\"/root/disabled/hello2/{param1}\".get", nullValue())
87+
.body("paths.\"/root/enabled/hello3\".get", notNullValue())
88+
.body("paths.\"/root/enabled/hello4/{param1}\".get", notNullValue())
89+
.body("paths.\"/root/enabled/hello5\".post", notNullValue())
90+
.body("paths.\"/root/enabled/hello5\".put", nullValue());
91+
}
92+
93+
}

extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class OpenApiWithResteasyPathHttpRootDefaultPathTestCase {
1616
.withApplicationRoot((jar) -> jar
1717
.addClasses(OpenApiResource.class, ResourceBean.class)
1818
.addAsResource(new StringAsset("quarkus.http.root-path=/http-root-path\n" +
19-
"quarkus.resteasy.path=/resteasy-path"),
19+
"quarkus.resteasy-reactive.path=/resteasy-path"),
2020
"application.properties"));
2121

2222
@Test

extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class OpenApiWithResteasyPathTestCase {
1515
static QuarkusUnitTest runner = new QuarkusUnitTest()
1616
.withApplicationRoot((jar) -> jar
1717
.addClasses(OpenApiResource.class, ResourceBean.class)
18-
.addAsResource(new StringAsset("quarkus.resteasy.path=/foo/bar"),
18+
.addAsResource(new StringAsset("quarkus.resteasy-reactive.path=/foo/bar"),
1919
"application.properties"));
2020

2121
@Test

extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.eclipse.microprofile.openapi.models.OpenAPI;
1313

1414
import io.quarkus.runtime.ShutdownEvent;
15+
import io.quarkus.smallrye.openapi.runtime.filter.DisabledRestEndpointsFilter;
1516
import io.smallrye.openapi.api.OpenApiConfig;
1617
import io.smallrye.openapi.api.OpenApiConfigImpl;
1718
import io.smallrye.openapi.api.OpenApiDocument;
@@ -88,6 +89,7 @@ static class StaticDocument implements OpenApiDocumentHolder {
8889
if (autoFilter != null) {
8990
document.filter(autoFilter);
9091
}
92+
document.filter(new DisabledRestEndpointsFilter());
9193
document.filter(OpenApiProcessor.getFilter(openApiConfig, cl));
9294
document.initialize();
9395

@@ -122,6 +124,7 @@ static class DynamicDocument implements OpenApiDocumentHolder {
122124
private OpenApiConfig openApiConfig;
123125
private OASFilter userFilter;
124126
private OASFilter autoFilter;
127+
private DisabledRestEndpointsFilter disabledEndpointsFilter;
125128

126129
DynamicDocument(Config config, OASFilter autoFilter) {
127130
ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader()
@@ -133,6 +136,7 @@ static class DynamicDocument implements OpenApiDocumentHolder {
133136
this.userFilter = OpenApiProcessor.getFilter(openApiConfig, cl);
134137
this.autoFilter = autoFilter;
135138
this.generatedOnBuild = OpenApiProcessor.modelFromStaticFile(this.openApiConfig, staticFile);
139+
this.disabledEndpointsFilter = new DisabledRestEndpointsFilter();
136140
}
137141
}
138142
} catch (IOException ex) {
@@ -174,6 +178,7 @@ private OpenApiDocument getOpenApiDocument() {
174178
if (this.autoFilter != null) {
175179
document.filter(this.autoFilter);
176180
}
181+
document.filter(this.disabledEndpointsFilter);
177182
document.filter(this.userFilter);
178183
document.initialize();
179184
return document;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.quarkus.smallrye.openapi.runtime.filter;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
7+
import org.eclipse.microprofile.openapi.OASFilter;
8+
import org.eclipse.microprofile.openapi.models.OpenAPI;
9+
import org.eclipse.microprofile.openapi.models.PathItem;
10+
11+
import io.quarkus.runtime.rest.DisabledRestEndpoints;
12+
13+
/**
14+
* If the RESTEasy Reactive extension passed us a list of REST paths that are disabled via the @DisabledRestEndpoint
15+
* annotation, remove them from the OpenAPI document. This has to be done at runtime because
16+
* the annotation is controlled by a runtime config property.
17+
*/
18+
public class DisabledRestEndpointsFilter implements OASFilter {
19+
20+
public void filterOpenAPI(OpenAPI openAPI) {
21+
Map<String, List<String>> disabledEndpointsMap = DisabledRestEndpoints.get();
22+
if (disabledEndpointsMap != null) {
23+
Map<String, PathItem> pathItems = openAPI.getPaths().getPathItems();
24+
List<String> emptyPathItems = new ArrayList<>();
25+
if (pathItems != null) {
26+
for (Map.Entry<String, PathItem> entry : pathItems.entrySet()) {
27+
String path = entry.getKey();
28+
PathItem pathItem = entry.getValue();
29+
List<String> disabledMethodsForThisPath = disabledEndpointsMap.get(path);
30+
if (disabledMethodsForThisPath != null) {
31+
disabledMethodsForThisPath.forEach(method -> {
32+
pathItem.setOperation(PathItem.HttpMethod.valueOf(method), null);
33+
});
34+
// if the pathItem is now empty, remove it
35+
if (pathItem.getOperations().isEmpty()) {
36+
emptyPathItems.add(path);
37+
}
38+
}
39+
}
40+
emptyPathItems.forEach(openAPI.getPaths()::removePathItem);
41+
}
42+
}
43+
}
44+
}

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/Deployment.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.ArrayList;
88
import java.util.Collection;
99
import java.util.List;
10+
import java.util.Map;
1011
import java.util.function.Supplier;
1112

1213
import jakarta.ws.rs.core.Application;
@@ -48,6 +49,7 @@ public class Deployment {
4849
private final RuntimeExceptionMapper exceptionMapper;
4950
private final boolean resumeOn404;
5051
private final ResteasyReactiveConfig resteasyReactiveConfig;
52+
private final Map<String, List<String>> disabledEndpoints;
5153
//this is not final, as it is set after startup
5254
private RuntimeConfiguration runtimeConfiguration;
5355

@@ -62,7 +64,8 @@ public Deployment(ExceptionMapping exceptionMapping, ContextResolvers contextRes
6264
List<GenericRuntimeConfigurableServerRestHandler<?>> runtimeConfigurableServerRestHandlers,
6365
RuntimeExceptionMapper exceptionMapper,
6466
boolean resumeOn404,
65-
ResteasyReactiveConfig resteasyReactiveConfig) {
67+
ResteasyReactiveConfig resteasyReactiveConfig,
68+
Map<String, List<String>> disabledEndpoints) {
6669
this.exceptionMapping = exceptionMapping;
6770
this.contextResolvers = contextResolvers;
6871
this.serialisers = serialisers;
@@ -80,6 +83,7 @@ public Deployment(ExceptionMapping exceptionMapping, ContextResolvers contextRes
8083
this.exceptionMapper = exceptionMapper;
8184
this.resumeOn404 = resumeOn404;
8285
this.resteasyReactiveConfig = resteasyReactiveConfig;
86+
this.disabledEndpoints = disabledEndpoints;
8387
}
8488

8589
public RuntimeExceptionMapper getExceptionMapper() {
@@ -206,4 +210,8 @@ public Deployment setRuntimeConfiguration(RuntimeConfiguration runtimeConfigurat
206210
this.runtimeConfiguration = runtimeConfiguration;
207211
return this;
208212
}
213+
214+
public Map<String, List<String>> getDisabledEndpoints() {
215+
return disabledEndpoints;
216+
}
209217
}

independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,31 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
104104
return info.getFactoryCreator().apply(aClass).createInstance();
105105
}
106106
});
107+
108+
// sanitise the prefix for our usage to make it either an empty string, or something which starts with a / and does not
109+
// end with one
110+
String prefix = rootPath;
111+
if (prefix != null) {
112+
prefix = sanitizePathPrefix(prefix);
113+
} else {
114+
prefix = "";
115+
}
116+
if ((applicationPath != null) && !applicationPath.isEmpty()) {
117+
prefix = prefix + sanitizePathPrefix(applicationPath);
118+
}
119+
// to use it inside lambdas
120+
String finalPrefix = prefix;
121+
107122
List<GenericRuntimeConfigurableServerRestHandler<?>> runtimeConfigurableServerRestHandlers = new ArrayList<>();
108123
RuntimeResourceDeployment runtimeResourceDeployment = new RuntimeResourceDeployment(info, executorSupplier,
109124
virtualExecutorSupplier,
110125
interceptorDeployment, dynamicEntityWriter, resourceLocatorHandler, requestContextFactory.isDefaultBlocking());
111126
List<ResourceClass> possibleSubResource = new ArrayList<>(locatableResourceClasses);
112127
possibleSubResource.addAll(resourceClasses); //the TCK uses normal resources also as sub resources
128+
Map<String, List<String>> disabledEndpoints = new HashMap<>();
113129
for (int i = 0; i < possibleSubResource.size(); i++) {
114130
ResourceClass clazz = possibleSubResource.get(i);
115-
if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) {
116-
continue;
117-
}
131+
118132
Map<String, TreeMap<URITemplate, List<RequestMapper.RequestPath<RuntimeResource>>>> templates = new HashMap<>();
119133
URITemplate classPathTemplate = clazz.getPath() == null ? null : new URITemplate(clazz.getPath(), true);
120134
for (int j = 0; j < clazz.getMethods().size(); j++) {
@@ -127,6 +141,24 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
127141
}
128142
Map<String, RequestMapper<RuntimeResource>> mappersByMethod = new RuntimeMappingDeployment(templates)
129143
.buildClassMapper();
144+
mappersByMethod.forEach((method, mapper) -> {
145+
for (RequestMapper.RequestPath<RuntimeResource> path : mapper.getTemplates()) {
146+
if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) {
147+
String templateWithoutSlash = path.template.template.startsWith("/")
148+
? path.template.template.substring(1)
149+
: path.template.template;
150+
String fullPath = clazz.getPath().endsWith("/") ? finalPrefix + clazz.getPath() + templateWithoutSlash
151+
: finalPrefix + clazz.getPath() + "/" + templateWithoutSlash;
152+
if (!disabledEndpoints.containsKey(fullPath)) {
153+
disabledEndpoints.put(fullPath, new ArrayList<>());
154+
}
155+
disabledEndpoints.get(fullPath).add(method);
156+
}
157+
}
158+
});
159+
if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) {
160+
continue;
161+
}
130162
resourceLocatorHandler.addResource(loadClass(clazz.getClassName()), mappersByMethod);
131163
}
132164

@@ -172,17 +204,6 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
172204
abortHandlingChain.addAll(interceptorDeployment.getGlobalResponseInterceptorHandlers());
173205
}
174206
abortHandlingChain.add(new ResponseWriterHandler(dynamicEntityWriter));
175-
// sanitise the prefix for our usage to make it either an empty string, or something which starts with a / and does not
176-
// end with one
177-
String prefix = rootPath;
178-
if (prefix != null) {
179-
prefix = sanitizePathPrefix(prefix);
180-
} else {
181-
prefix = "";
182-
}
183-
if ((applicationPath != null) && !applicationPath.isEmpty()) {
184-
prefix = prefix + sanitizePathPrefix(applicationPath);
185-
}
186207

187208
//pre matching interceptors are run first
188209
List<ServerRestHandler> preMatchHandlers = new ArrayList<>();
@@ -210,7 +231,8 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
210231
abortHandlingChain.toArray(EMPTY_REST_HANDLER_ARRAY), dynamicEntityWriter,
211232
prefix, paramConverterProviders, configurationImpl, applicationSupplier,
212233
threadSetupAction, requestContextFactory, preMatchHandlers, classMappers,
213-
runtimeConfigurableServerRestHandlers, exceptionMapper, info.isResumeOn404(), info.getResteasyReactiveConfig());
234+
runtimeConfigurableServerRestHandlers, exceptionMapper, info.isResumeOn404(), info.getResteasyReactiveConfig(),
235+
disabledEndpoints);
214236
}
215237

216238
private void forEachMapperEntry(MappersKey key,

0 commit comments

Comments
 (0)