Skip to content

Commit 9145e15

Browse files
committed
Allow excluding individual methods from OpenApi output
It is currently possible to prevent methods from being included in the OpenAPI output by ensuring the parent class is not covered by the `packagesToScan` or `pathsToMatch` configuration, but where a method that a user doesn't want included in the output is present in a class that is included in the configuration, and that method shares a path with other endpoints that also require inclusion, there's no way for a user to exclude the method from being exposed as an operation in the OpenAPI output. To overcome this limitation, a MethodFilter has been introduced which allows for a user to selectively exclude individual methods from being parsed for their definitions. Every method that's detected for potential inclusion in the OpenApi output is passed to the filter, and any method that the filter rejects is excluded from further processing. As multiple filters can be applied in a single configuration, the result of all the filters is combined in an 'and' result, so all filters must accept a method the have it available in the output. This functionality allows for the annotation filtering feature in SpringFox to be re-implemented by users, by creating a method filter similar to `method -> method.isAnnotationPresent(MyAnnotation.class)`, although allows much finer control since any other reflective attributes of the method can also be checked at the same time.
1 parent 5787a07 commit 9145e15

File tree

11 files changed

+439
-5
lines changed

11 files changed

+439
-5
lines changed

springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,10 +650,27 @@ protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVis
650650
* @return the boolean
651651
*/
652652
protected boolean isFilterCondition(HandlerMethod handlerMethod, String operationPath, String[] produces, String[] consumes, String[] headers) {
653-
return isPackageToScan(handlerMethod.getBeanType().getPackage())
653+
return isSuitableTargetMethod(handlerMethod)
654+
&& isPackageToScan(handlerMethod.getBeanType().getPackage())
654655
&& isFilterCondition(operationPath, produces, consumes, headers);
655656
}
656657

658+
/**
659+
* Is target method suitable for inclusion in current documentation/
660+
*
661+
* @param handlerMethod the method to check
662+
* @return whether the method should be included in the current OpenAPI definition
663+
*/
664+
protected boolean isSuitableTargetMethod(HandlerMethod handlerMethod) {
665+
return springDocConfigProperties.getGroupConfigs().stream()
666+
.filter(groupConfig -> this.groupName.equals(groupConfig.getGroup()))
667+
.findAny()
668+
.map(GroupConfig::getMethodFilters)
669+
.map(Collection::stream)
670+
.map(stream -> stream.allMatch(m -> m.includeMethodInOpenApi(handlerMethod.getMethod())))
671+
.orElse(true);
672+
}
673+
657674
/**
658675
* Is condition to match boolean.
659676
*

springdoc-openapi-common/src/main/java/org/springdoc/core/GroupedOpenApi.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ public class GroupedOpenApi {
8888
*/
8989
private final List<String> consumesToMatch;
9090

91+
/**
92+
* The method filters to use.
93+
*/
94+
private final List<MethodFilter> methodFilters;
95+
9196
/**
9297
* Instantiates a new Grouped open api.
9398
*
@@ -104,6 +109,7 @@ private GroupedOpenApi(Builder builder) {
104109
this.pathsToExclude = builder.pathsToExclude;
105110
this.openApiCustomisers = Objects.requireNonNull(builder.openApiCustomisers);
106111
this.operationCustomizers = Objects.requireNonNull(builder.operationCustomizers);
112+
this.methodFilters = Objects.requireNonNull(builder.methodFilters);
107113
if (CollectionUtils.isEmpty(this.pathsToMatch)
108114
&& CollectionUtils.isEmpty(this.packagesToScan)
109115
&& CollectionUtils.isEmpty(this.producesToMatch)
@@ -112,7 +118,8 @@ private GroupedOpenApi(Builder builder) {
112118
&& CollectionUtils.isEmpty(this.pathsToExclude)
113119
&& CollectionUtils.isEmpty(this.packagesToExclude)
114120
&& CollectionUtils.isEmpty(openApiCustomisers)
115-
&& CollectionUtils.isEmpty(operationCustomizers))
121+
&& CollectionUtils.isEmpty(operationCustomizers)
122+
&& CollectionUtils.isEmpty(methodFilters))
116123
throw new IllegalStateException("Packages to scan or paths to filter or openApiCustomisers/operationCustomizers can not be all null for the group:" + this.group);
117124
}
118125

@@ -215,6 +222,15 @@ public List<OperationCustomizer> getOperationCustomizers() {
215222
return operationCustomizers;
216223
}
217224

225+
/**
226+
* Gets method filters.
227+
*
228+
* @return the method filters
229+
*/
230+
public List<MethodFilter> getMethodFilters() {
231+
return methodFilters;
232+
}
233+
218234
/**
219235
* The type Builder.
220236
* @author bnasslahsen
@@ -230,6 +246,11 @@ public static class Builder {
230246
*/
231247
private final List<OperationCustomizer> operationCustomizers = new ArrayList<>();
232248

249+
/**
250+
* The methods filters to apply.
251+
*/
252+
private final List<MethodFilter> methodFilters = new ArrayList<>();
253+
233254
/**
234255
* The Group.
235256
*/
@@ -387,6 +408,17 @@ public Builder addOperationCustomizer(OperationCustomizer operationCustomizer) {
387408
return this;
388409
}
389410

411+
/**
412+
* Add method filter.
413+
*
414+
* @param methodFilter an additional filter to apply to the matched methods
415+
* @return the builder
416+
*/
417+
public Builder addMethodFilter(MethodFilter methodFilter) {
418+
this.methodFilters.add(methodFilter);
419+
return this;
420+
}
421+
390422
/**
391423
* Build grouped open api.
392424
*
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
*
3+
* *
4+
* * * Copyright 2019-2020 the original author or authors.
5+
* * *
6+
* * * Licensed under the Apache License, Version 2.0 (the "License");
7+
* * * you may not use this file except in compliance with the License.
8+
* * * You may obtain a copy of the License at
9+
* * *
10+
* * * https://www.apache.org/licenses/LICENSE-2.0
11+
* * *
12+
* * * Unless required by applicable law or agreed to in writing, software
13+
* * * distributed under the License is distributed on an "AS IS" BASIS,
14+
* * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* * * See the License for the specific language governing permissions and
16+
* * * limitations under the License.
17+
* *
18+
*
19+
*/
20+
21+
package org.springdoc.core;
22+
23+
import java.lang.reflect.Method;
24+
25+
/**
26+
* A filter to allow conditionally including any detected methods in an OpenApi definition.
27+
* @author michael.clarke
28+
*/
29+
@FunctionalInterface
30+
public interface MethodFilter {
31+
32+
/**
33+
* Whether the given method should be included in the generated OpenApi definitions. Only methods from classes
34+
* detected by the relevant loader will be passed to this filter; it cannot be used to load methods that are not
35+
* annotated with `RequestMethod` or similar mechanisms. Methods that are rejected by this filter will not be
36+
* processed any further, although methods accepted by this filter may still be rejected by other checks, such as
37+
* package inclusion checks so may still be excluded from the final OpenApi definition.
38+
*
39+
* @param method the method to perform checks against
40+
* @return whether this method should be used for further processing
41+
*/
42+
boolean includeMethodInOpenApi(Method method);
43+
}

springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfigProperties.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,11 @@ public static class GroupConfig {
10811081
*/
10821082
private List<String> consumesToMatch;
10831083

1084+
/**
1085+
* The method filters to use.
1086+
*/
1087+
private List<MethodFilter> methodFilters;
1088+
10841089
/**
10851090
* Instantiates a new Group config.
10861091
*/
@@ -1098,10 +1103,32 @@ public GroupConfig() {
10981103
* @param producesToMatch the produces to match
10991104
* @param consumesToMatch the consumes to match
11001105
* @param headersToMatch the headers to match
1106+
* @deprecated Use {@link #GroupConfig(String, List, List, List, List, List, List, List, List)}
11011107
*/
1108+
@Deprecated
11021109
public GroupConfig(String group, List<String> pathsToMatch, List<String> packagesToScan,
11031110
List<String> packagesToExclude, List<String> pathsToExclude,
1104-
List<String> producesToMatch,List<String> consumesToMatch,List<String> headersToMatch) {
1111+
List<String> producesToMatch, List<String> consumesToMatch, List<String> headersToMatch) {
1112+
this(group, pathsToMatch, packagesToScan, packagesToExclude, pathsToExclude, producesToMatch, consumesToMatch, headersToMatch, new ArrayList<>());
1113+
}
1114+
1115+
/**
1116+
* Instantiates a new Group config.
1117+
*
1118+
* @param group the group
1119+
* @param pathsToMatch the paths to match
1120+
* @param packagesToScan the packages to scan
1121+
* @param packagesToExclude the packages to exclude
1122+
* @param pathsToExclude the paths to exclude
1123+
* @param producesToMatch the produces to match
1124+
* @param consumesToMatch the consumes to match
1125+
* @param headersToMatch the headers to match
1126+
* @param methodFilters the method filters to use
1127+
*/
1128+
public GroupConfig(String group, List<String> pathsToMatch, List<String> packagesToScan,
1129+
List<String> packagesToExclude, List<String> pathsToExclude,
1130+
List<String> producesToMatch, List<String> consumesToMatch, List<String> headersToMatch,
1131+
List<MethodFilter> methodFilters) {
11051132
this.pathsToMatch = pathsToMatch;
11061133
this.pathsToExclude = pathsToExclude;
11071134
this.packagesToExclude = packagesToExclude;
@@ -1110,6 +1137,7 @@ public GroupConfig(String group, List<String> pathsToMatch, List<String> package
11101137
this.producesToMatch = producesToMatch;
11111138
this.consumesToMatch = consumesToMatch;
11121139
this.headersToMatch = headersToMatch;
1140+
this.methodFilters = methodFilters;
11131141
}
11141142

11151143
/**
@@ -1255,5 +1283,23 @@ public List<String> getProducesToMatch() {
12551283
public void setProducesToMatch(List<String> producesToMatch) {
12561284
this.producesToMatch = producesToMatch;
12571285
}
1286+
1287+
/**
1288+
* Gets the method filters to use.
1289+
*
1290+
* @return the method filters to use
1291+
*/
1292+
public List<MethodFilter> getMethodFilters() {
1293+
return methodFilters;
1294+
}
1295+
1296+
/**
1297+
* Sets the method filters to use.
1298+
*
1299+
* @param methodFilters the method filters to use
1300+
*/
1301+
public void setMethodFilters(List<MethodFilter> methodFilters) {
1302+
this.methodFilters = methodFilters;
1303+
}
12581304
}
12591305
}

springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/api/MultipleOpenApiResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public void afterPropertiesSet() {
116116
this.groupedOpenApiResources = groupedOpenApis.stream()
117117
.collect(Collectors.toMap(GroupedOpenApi::getGroup, item ->
118118
{
119-
GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(),item.getHeadersToMatch());
119+
GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(),item.getHeadersToMatch(), item.getMethodFilters());
120120
springDocConfigProperties.addGroupConfig(groupConfig);
121121
return buildWebFluxOpenApiResource(item);
122122
}

springdoc-openapi-webmvc-core/src/main/java/org/springdoc/webmvc/api/MultipleOpenApiResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public void afterPropertiesSet() {
115115
this.groupedOpenApiResources = groupedOpenApis.stream()
116116
.collect(Collectors.toMap(GroupedOpenApi::getGroup, item ->
117117
{
118-
GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(), item.getHeadersToMatch());
118+
GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(), item.getHeadersToMatch(), item.getMethodFilters());
119119
springDocConfigProperties.addGroupConfig(groupConfig);
120120
return buildWebMvcOpenApiResource(item);
121121
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package test.org.springdoc.api.app177;
2+
3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.RetentionPolicy;
5+
6+
import org.springdoc.core.GroupedOpenApi;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.PutMapping;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
@RestController
14+
public class AnnotatedController {
15+
16+
@Group1
17+
@GetMapping("/annotated")
18+
public String annotatedGet() {
19+
return "annotated";
20+
}
21+
22+
@Group1
23+
@PostMapping("/annotated")
24+
public String annotatedPost() {
25+
return "annotated";
26+
}
27+
28+
@Group2
29+
@PutMapping("/annotated")
30+
public String annotatedPut() {
31+
return "annotated";
32+
}
33+
34+
@Bean
35+
public GroupedOpenApi group1OpenApi() {
36+
return GroupedOpenApi.builder()
37+
.group("annotatedGroup1")
38+
.addMethodFilter(method -> method.isAnnotationPresent(Group1.class))
39+
.build();
40+
}
41+
42+
@Bean
43+
public GroupedOpenApi group2OpenApi() {
44+
return GroupedOpenApi.builder()
45+
.group("annotatedGroup2")
46+
.addMethodFilter(method -> method.isAnnotationPresent(Group2.class))
47+
.build();
48+
}
49+
50+
@Bean
51+
public GroupedOpenApi group3OpenApi() {
52+
return GroupedOpenApi.builder()
53+
.group("annotatedCombinedGroup")
54+
.addMethodFilter(method -> method.isAnnotationPresent(Group1.class) || method.isAnnotationPresent(Group2.class))
55+
.build();
56+
}
57+
58+
@Retention(RetentionPolicy.RUNTIME)
59+
@interface Group1 {
60+
61+
}
62+
63+
@Retention(RetentionPolicy.RUNTIME)
64+
@interface Group2 {
65+
66+
}
67+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * * Copyright 2019-2020 the original author or authors.
6+
* * * *
7+
* * * * Licensed under the Apache License, Version 2.0 (the "License");
8+
* * * * you may not use this file except in compliance with the License.
9+
* * * * You may obtain a copy of the License at
10+
* * * *
11+
* * * * https://www.apache.org/licenses/LICENSE-2.0
12+
* * * *
13+
* * * * Unless required by applicable law or agreed to in writing, software
14+
* * * * distributed under the License is distributed on an "AS IS" BASIS,
15+
* * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* * * * See the License for the specific language governing permissions and
17+
* * * * limitations under the License.
18+
* * *
19+
* *
20+
*
21+
*
22+
*/
23+
24+
package test.org.springdoc.api.app177;
25+
26+
import static org.hamcrest.Matchers.is;
27+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
28+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
29+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
30+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
31+
32+
import org.junit.jupiter.api.Test;
33+
import org.springdoc.core.Constants;
34+
import org.springframework.boot.autoconfigure.SpringBootApplication;
35+
36+
import test.org.springdoc.api.AbstractSpringDocTest;
37+
38+
class SpringDocApp177Test extends AbstractSpringDocTest {
39+
40+
@SpringBootApplication
41+
static class SpringDocTestApp {}
42+
43+
@Test
44+
void testFilterOnlyPicksUpMatchedMethods() throws Exception {
45+
mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedGroup1"))
46+
.andExpect(status().isOk())
47+
.andExpect(jsonPath("$.openapi", is("3.0.1")))
48+
.andExpect(content().json(getContent("results/app177-1.json"), true));
49+
}
50+
51+
@Test
52+
void testFilterOnlyPicksUpMatchedMethodsWithDifferentFilter() throws Exception {
53+
mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedGroup2"))
54+
.andExpect(status().isOk())
55+
.andExpect(jsonPath("$.openapi", is("3.0.1")))
56+
.andExpect(content().json(getContent("results/app177-2.json"), true));
57+
}
58+
59+
@Test
60+
void testFilterOnlyPicksUpCombinedMatchedMethods() throws Exception {
61+
mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedCombinedGroup"))
62+
.andExpect(status().isOk())
63+
.andExpect(jsonPath("$.openapi", is("3.0.1")))
64+
.andExpect(content().json(getContent("results/app177.json"), true));
65+
}
66+
67+
}

0 commit comments

Comments
 (0)