diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java index 16001f400..bba09c491 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java @@ -650,10 +650,27 @@ protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVis * @return the boolean */ protected boolean isFilterCondition(HandlerMethod handlerMethod, String operationPath, String[] produces, String[] consumes, String[] headers) { - return isPackageToScan(handlerMethod.getBeanType().getPackage()) + return isSuitableTargetMethod(handlerMethod) + && isPackageToScan(handlerMethod.getBeanType().getPackage()) && isFilterCondition(operationPath, produces, consumes, headers); } + /** + * Is target method suitable for inclusion in current documentation/ + * + * @param handlerMethod the method to check + * @return whether the method should be included in the current OpenAPI definition + */ + protected boolean isSuitableTargetMethod(HandlerMethod handlerMethod) { + return springDocConfigProperties.getGroupConfigs().stream() + .filter(groupConfig -> this.groupName.equals(groupConfig.getGroup())) + .findAny() + .map(GroupConfig::getMethodFilters) + .map(Collection::stream) + .map(stream -> stream.allMatch(m -> m.includeMethodInOpenApi(handlerMethod.getMethod()))) + .orElse(true); + } + /** * Is condition to match boolean. * diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/GroupedOpenApi.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/GroupedOpenApi.java index 09f4947c2..7dcb25f0e 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/core/GroupedOpenApi.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/GroupedOpenApi.java @@ -88,6 +88,11 @@ public class GroupedOpenApi { */ private final List consumesToMatch; + /** + * The method filters to use. + */ + private final List methodFilters; + /** * Instantiates a new Grouped open api. * @@ -104,6 +109,7 @@ private GroupedOpenApi(Builder builder) { this.pathsToExclude = builder.pathsToExclude; this.openApiCustomisers = Objects.requireNonNull(builder.openApiCustomisers); this.operationCustomizers = Objects.requireNonNull(builder.operationCustomizers); + this.methodFilters = Objects.requireNonNull(builder.methodFilters); if (CollectionUtils.isEmpty(this.pathsToMatch) && CollectionUtils.isEmpty(this.packagesToScan) && CollectionUtils.isEmpty(this.producesToMatch) @@ -112,7 +118,8 @@ private GroupedOpenApi(Builder builder) { && CollectionUtils.isEmpty(this.pathsToExclude) && CollectionUtils.isEmpty(this.packagesToExclude) && CollectionUtils.isEmpty(openApiCustomisers) - && CollectionUtils.isEmpty(operationCustomizers)) + && CollectionUtils.isEmpty(operationCustomizers) + && CollectionUtils.isEmpty(methodFilters)) throw new IllegalStateException("Packages to scan or paths to filter or openApiCustomisers/operationCustomizers can not be all null for the group:" + this.group); } @@ -215,6 +222,15 @@ public List getOperationCustomizers() { return operationCustomizers; } + /** + * Gets method filters. + * + * @return the method filters + */ + public List getMethodFilters() { + return methodFilters; + } + /** * The type Builder. * @author bnasslahsen @@ -230,6 +246,11 @@ public static class Builder { */ private final List operationCustomizers = new ArrayList<>(); + /** + * The methods filters to apply. + */ + private final List methodFilters = new ArrayList<>(); + /** * The Group. */ @@ -387,6 +408,17 @@ public Builder addOperationCustomizer(OperationCustomizer operationCustomizer) { return this; } + /** + * Add method filter. + * + * @param methodFilter an additional filter to apply to the matched methods + * @return the builder + */ + public Builder addMethodFilter(MethodFilter methodFilter) { + this.methodFilters.add(methodFilter); + return this; + } + /** * Build grouped open api. * diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/MethodFilter.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/MethodFilter.java new file mode 100644 index 000000000..8a71e8c07 --- /dev/null +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/MethodFilter.java @@ -0,0 +1,43 @@ +/* + * + * * + * * * Copyright 2019-2020 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.springdoc.core; + +import java.lang.reflect.Method; + +/** + * A filter to allow conditionally including any detected methods in an OpenApi definition. + * @author michael.clarke + */ +@FunctionalInterface +public interface MethodFilter { + + /** + * Whether the given method should be included in the generated OpenApi definitions. Only methods from classes + * detected by the relevant loader will be passed to this filter; it cannot be used to load methods that are not + * annotated with `RequestMethod` or similar mechanisms. Methods that are rejected by this filter will not be + * processed any further, although methods accepted by this filter may still be rejected by other checks, such as + * package inclusion checks so may still be excluded from the final OpenApi definition. + * + * @param method the method to perform checks against + * @return whether this method should be used for further processing + */ + boolean includeMethodInOpenApi(Method method); +} diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfigProperties.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfigProperties.java index d3532e1d7..5c7564f87 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfigProperties.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfigProperties.java @@ -1081,6 +1081,11 @@ public static class GroupConfig { */ private List consumesToMatch; + /** + * The method filters to use. + */ + private List methodFilters; + /** * Instantiates a new Group config. */ @@ -1098,10 +1103,32 @@ public GroupConfig() { * @param producesToMatch the produces to match * @param consumesToMatch the consumes to match * @param headersToMatch the headers to match + * @deprecated Use {@link #GroupConfig(String, List, List, List, List, List, List, List, List)} */ + @Deprecated public GroupConfig(String group, List pathsToMatch, List packagesToScan, List packagesToExclude, List pathsToExclude, - List producesToMatch,List consumesToMatch,List headersToMatch) { + List producesToMatch, List consumesToMatch, List headersToMatch) { + this(group, pathsToMatch, packagesToScan, packagesToExclude, pathsToExclude, producesToMatch, consumesToMatch, headersToMatch, new ArrayList<>()); + } + + /** + * Instantiates a new Group config. + * + * @param group the group + * @param pathsToMatch the paths to match + * @param packagesToScan the packages to scan + * @param packagesToExclude the packages to exclude + * @param pathsToExclude the paths to exclude + * @param producesToMatch the produces to match + * @param consumesToMatch the consumes to match + * @param headersToMatch the headers to match + * @param methodFilters the method filters to use + */ + public GroupConfig(String group, List pathsToMatch, List packagesToScan, + List packagesToExclude, List pathsToExclude, + List producesToMatch, List consumesToMatch, List headersToMatch, + List methodFilters) { this.pathsToMatch = pathsToMatch; this.pathsToExclude = pathsToExclude; this.packagesToExclude = packagesToExclude; @@ -1110,6 +1137,7 @@ public GroupConfig(String group, List pathsToMatch, List package this.producesToMatch = producesToMatch; this.consumesToMatch = consumesToMatch; this.headersToMatch = headersToMatch; + this.methodFilters = methodFilters; } /** @@ -1255,5 +1283,23 @@ public List getProducesToMatch() { public void setProducesToMatch(List producesToMatch) { this.producesToMatch = producesToMatch; } + + /** + * Gets the method filters to use. + * + * @return the method filters to use + */ + public List getMethodFilters() { + return methodFilters; + } + + /** + * Sets the method filters to use. + * + * @param methodFilters the method filters to use + */ + public void setMethodFilters(List methodFilters) { + this.methodFilters = methodFilters; + } } } diff --git a/springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/api/MultipleOpenApiResource.java b/springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/api/MultipleOpenApiResource.java index 59189df6f..0190af76d 100644 --- a/springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/api/MultipleOpenApiResource.java +++ b/springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/api/MultipleOpenApiResource.java @@ -116,7 +116,7 @@ public void afterPropertiesSet() { this.groupedOpenApiResources = groupedOpenApis.stream() .collect(Collectors.toMap(GroupedOpenApi::getGroup, item -> { - GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(),item.getHeadersToMatch()); + GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(),item.getHeadersToMatch(), item.getMethodFilters()); springDocConfigProperties.addGroupConfig(groupConfig); return buildWebFluxOpenApiResource(item); } diff --git a/springdoc-openapi-webmvc-core/src/main/java/org/springdoc/webmvc/api/MultipleOpenApiResource.java b/springdoc-openapi-webmvc-core/src/main/java/org/springdoc/webmvc/api/MultipleOpenApiResource.java index 52178b1aa..b9aee5141 100644 --- a/springdoc-openapi-webmvc-core/src/main/java/org/springdoc/webmvc/api/MultipleOpenApiResource.java +++ b/springdoc-openapi-webmvc-core/src/main/java/org/springdoc/webmvc/api/MultipleOpenApiResource.java @@ -115,7 +115,7 @@ public void afterPropertiesSet() { this.groupedOpenApiResources = groupedOpenApis.stream() .collect(Collectors.toMap(GroupedOpenApi::getGroup, item -> { - GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(), item.getHeadersToMatch()); + GroupConfig groupConfig = new GroupConfig(item.getGroup(), item.getPathsToMatch(), item.getPackagesToScan(), item.getPackagesToExclude(), item.getPathsToExclude(), item.getProducesToMatch(), item.getConsumesToMatch(), item.getHeadersToMatch(), item.getMethodFilters()); springDocConfigProperties.addGroupConfig(groupConfig); return buildWebMvcOpenApiResource(item); } diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app177/AnnotatedController.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app177/AnnotatedController.java new file mode 100644 index 000000000..c2924dea6 --- /dev/null +++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app177/AnnotatedController.java @@ -0,0 +1,67 @@ +package test.org.springdoc.api.app177; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AnnotatedController { + + @Group1 + @GetMapping("/annotated") + public String annotatedGet() { + return "annotated"; + } + + @Group1 + @PostMapping("/annotated") + public String annotatedPost() { + return "annotated"; + } + + @Group2 + @PutMapping("/annotated") + public String annotatedPut() { + return "annotated"; + } + + @Bean + public GroupedOpenApi group1OpenApi() { + return GroupedOpenApi.builder() + .group("annotatedGroup1") + .addMethodFilter(method -> method.isAnnotationPresent(Group1.class)) + .build(); + } + + @Bean + public GroupedOpenApi group2OpenApi() { + return GroupedOpenApi.builder() + .group("annotatedGroup2") + .addMethodFilter(method -> method.isAnnotationPresent(Group2.class)) + .build(); + } + + @Bean + public GroupedOpenApi group3OpenApi() { + return GroupedOpenApi.builder() + .group("annotatedCombinedGroup") + .addMethodFilter(method -> method.isAnnotationPresent(Group1.class) || method.isAnnotationPresent(Group2.class)) + .build(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Group1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Group2 { + + } +} diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app177/SpringDocApp177Test.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app177/SpringDocApp177Test.java new file mode 100644 index 000000000..c1ef58c2f --- /dev/null +++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app177/SpringDocApp177Test.java @@ -0,0 +1,67 @@ +/* + * + * * + * * * + * * * * Copyright 2019-2020 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 test.org.springdoc.api.app177; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springdoc.core.Constants; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import test.org.springdoc.api.AbstractSpringDocTest; + +class SpringDocApp177Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp {} + + @Test + void testFilterOnlyPicksUpMatchedMethods() throws Exception { + mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedGroup1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.openapi", is("3.0.1"))) + .andExpect(content().json(getContent("results/app177-1.json"), true)); + } + + @Test + void testFilterOnlyPicksUpMatchedMethodsWithDifferentFilter() throws Exception { + mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedGroup2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.openapi", is("3.0.1"))) + .andExpect(content().json(getContent("results/app177-2.json"), true)); + } + + @Test + void testFilterOnlyPicksUpCombinedMatchedMethods() throws Exception { + mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedCombinedGroup")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.openapi", is("3.0.1"))) + .andExpect(content().json(getContent("results/app177.json"), true)); + } + +} \ No newline at end of file diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app177-1.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app177-1.json new file mode 100644 index 000000000..a012c2f26 --- /dev/null +++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app177-1.json @@ -0,0 +1,54 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/annotated": { + "get": { + "tags": [ + "annotated-controller" + ], + "operationId": "annotatedGet", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "annotated-controller" + ], + "operationId": "annotatedPost", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": {} +} \ No newline at end of file diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app177-2.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app177-2.json new file mode 100644 index 000000000..9d9f99c6f --- /dev/null +++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app177-2.json @@ -0,0 +1,36 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/annotated": { + "put": { + "tags": [ + "annotated-controller" + ], + "operationId": "annotatedPut", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": {} +} \ No newline at end of file diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app177.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app177.json new file mode 100644 index 000000000..a08badbe5 --- /dev/null +++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app177.json @@ -0,0 +1,72 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/annotated": { + "get": { + "tags": [ + "annotated-controller" + ], + "operationId": "annotatedGet", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "put": { + "tags": [ + "annotated-controller" + ], + "operationId": "annotatedPut", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "annotated-controller" + ], + "operationId": "annotatedPost", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": {} +} \ No newline at end of file