Skip to content

Commit 347227a

Browse files
committed
Merge branch 'zdary-issue-3056'
2 parents 38df7e9 + da34e96 commit 347227a

File tree

8 files changed

+359
-60
lines changed

8 files changed

+359
-60
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
* @author kevinraddatz
141141
* @author hyeonisism
142142
* @author doljae
143+
* @author zdary
143144
*/
144145
public abstract class AbstractOpenApiResource extends SpecFilter {
145146

@@ -523,8 +524,12 @@ private void trimIndentOperation(Operation operation) {
523524
* @param locale the locale
524525
*/
525526
protected void calculateWebhooks(OpenAPI calculatedOpenAPI, Locale locale) {
526-
Webhooks[] webhooksAttr = openAPIService.getWebhooks();
527-
if (ArrayUtils.isEmpty(webhooksAttr))
527+
Class<?>[] classes = openAPIService.getWebhooksClasses();
528+
Class<?>[] refinedClasses = Arrays.stream(classes)
529+
.filter(clazz -> isPackageToScan(clazz.getPackage()))
530+
.toArray(Class<?>[]::new);
531+
Webhooks[] webhooksAttr = openAPIService.getWebhooks(refinedClasses);
532+
if (ArrayUtils.isEmpty(webhooksAttr))
528533
return;
529534
var webhooks = Arrays.stream(webhooksAttr).map(Webhooks::value).flatMap(Arrays::stream).toArray(Webhook[]::new);
530535
Arrays.stream(webhooks).forEach(webhook -> {

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OpenAPIService.java

Lines changed: 66 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
* The type Open api builder.
105105
*
106106
* @author bnasslahsen
107+
* @author zdary
107108
*/
108109
public class OpenAPIService implements ApplicationContextAware {
109110

@@ -538,63 +539,72 @@ private Optional<OpenAPIDefinition> getOpenAPIDefinition() {
538539
}
539540

540541

541-
/**
542-
* Get webhooks webhooks [ ].
543-
*
544-
* @return the webhooks [ ]
545-
*/
546-
public Webhooks[] getWebhooks() {
547-
List<Webhooks> allWebhooks = new ArrayList<>();
548-
549-
// First: scan Spring-managed beans
550-
Map<String, Object> beans = context.getBeansWithAnnotation(Webhooks.class);
551-
552-
for (Object bean : beans.values()) {
553-
Class<?> beanClass = bean.getClass();
554-
555-
// Collect @Webhooks or @Webhook on class level
556-
collectWebhooksFromElement(beanClass, allWebhooks);
542+
/**
543+
* Gets webhooks from given classes.
544+
*
545+
* @param classes Array of classes to scan for webhooks.
546+
* @return An array of {@link Webhooks} annotations found in the given classes.
547+
*/
548+
public Webhooks[] getWebhooks(Class<?>[] classes) {
549+
List<Webhooks> allWebhooks = new ArrayList<>();
550+
551+
for (Class<?> clazz : classes) {
552+
// Class-level annotations
553+
collectWebhooksFromElement(clazz, allWebhooks);
554+
555+
// Method-level annotations
556+
for (Method method : clazz.getDeclaredMethods()) {
557+
collectWebhooksFromElement(method, allWebhooks);
558+
}
559+
}
560+
561+
return allWebhooks.toArray(new Webhooks[0]);
562+
}
563+
564+
565+
/**
566+
* Retrieves all classes related to webhooks.
567+
* This method scans for classes annotated with {@link Webhooks} or {@link Webhook},
568+
* first checking Spring-managed beans and then falling back to classpath scanning
569+
* if no annotated beans are found.
570+
*
571+
* @return An array of classes related to webhooks.
572+
*/
573+
public Class<?>[] getWebhooksClasses() {
574+
Set<Class<?>> allWebhookClassesToScan = new HashSet<>();
575+
576+
// First: scan Spring-managed beans
577+
Map<String, Object> beans = context.getBeansWithAnnotation(Webhooks.class);
578+
579+
for (Object bean : beans.values()) {
580+
Class<?> beanClass = bean.getClass();
581+
allWebhookClassesToScan.add(beanClass);
582+
}
583+
584+
// Fallback: classpath scanning
585+
ClassPathScanningCandidateComponentProvider scanner =
586+
new ClassPathScanningCandidateComponentProvider(false);
587+
scanner.addIncludeFilter(new AnnotationTypeFilter(Webhooks.class));
588+
scanner.addIncludeFilter(new AnnotationTypeFilter(Webhook.class));
589+
590+
if (AutoConfigurationPackages.has(context)) {
591+
for (String basePackage : AutoConfigurationPackages.get(context)) {
592+
Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage);
593+
for (BeanDefinition bd : candidates) {
594+
try {
595+
Class<?> clazz = Class.forName(bd.getBeanClassName());
596+
allWebhookClassesToScan.add(clazz);
597+
}
598+
catch (ClassNotFoundException e) {
599+
LOGGER.error("Class not found in classpath: {}", e.getMessage());
600+
}
601+
}
602+
}
603+
}
604+
605+
return allWebhookClassesToScan.toArray(new Class<?>[0]);
606+
}
557607

558-
// Collect from methods
559-
for (Method method : beanClass.getDeclaredMethods()) {
560-
collectWebhooksFromElement(method, allWebhooks);
561-
}
562-
}
563-
564-
// Fallback: classpath scanning if nothing found
565-
if (allWebhooks.isEmpty()) {
566-
ClassPathScanningCandidateComponentProvider scanner =
567-
new ClassPathScanningCandidateComponentProvider(false);
568-
scanner.addIncludeFilter(new AnnotationTypeFilter(Webhooks.class));
569-
scanner.addIncludeFilter(new AnnotationTypeFilter(Webhook.class));
570-
571-
if (AutoConfigurationPackages.has(context)) {
572-
for (String basePackage : AutoConfigurationPackages.get(context)) {
573-
Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage);
574-
575-
for (BeanDefinition bd : candidates) {
576-
try {
577-
Class<?> clazz = Class.forName(bd.getBeanClassName());
578-
579-
// Class-level annotations
580-
collectWebhooksFromElement(clazz, allWebhooks);
581-
582-
// Method-level annotations
583-
for (Method method : clazz.getDeclaredMethods()) {
584-
collectWebhooksFromElement(method, allWebhooks);
585-
}
586-
587-
}
588-
catch (ClassNotFoundException e) {
589-
LOGGER.error("Class not found in classpath: {}", e.getMessage());
590-
}
591-
}
592-
}
593-
}
594-
}
595-
596-
return allWebhooks.toArray(new Webhooks[0]);
597-
}
598608

599609
/**
600610
* Collect webhooks from element.

springdoc-openapi-starter-common/src/test/java/org/springdoc/api/AbstractOpenApiResourceTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ public void setUp() {
125125

126126
when(openAPIService.build(any())).thenReturn(openAPI);
127127
when(openAPIService.getContext()).thenReturn(context);
128-
doAnswer(new CallsRealMethods()).when(openAPIService).setServersPresent(false);
128+
when(openAPIService.getWebhooksClasses()).thenReturn(new Class<?>[0]);
129+
doAnswer(new CallsRealMethods()).when(openAPIService).setServersPresent(false);
129130

130131
when(openAPIBuilderObjectFactory.getObject()).thenReturn(openAPIService);
131132
when(springDocProviders.jsonMapper()).thenReturn(Json.mapper());
@@ -295,4 +296,4 @@ private static class EmptyPathsOpenApiResource extends AbstractOpenApiResource {
295296
public void getPaths(Map<String, Object> findRestControllers, Locale locale, OpenAPI openAPI) {
296297
}
297298
}
298-
}
299+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2024 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v31;
26+
27+
import org.junit.jupiter.api.Test;
28+
import org.springdoc.core.utils.Constants;
29+
import test.org.springdoc.api.AbstractCommonTest;
30+
31+
import org.springframework.boot.test.context.SpringBootTest;
32+
import org.springframework.test.context.TestPropertySource;
33+
import org.springframework.test.web.servlet.MvcResult;
34+
35+
import static org.hamcrest.Matchers.is;
36+
import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
37+
import static org.springdoc.core.utils.Constants.SPRINGDOC_CACHE_DISABLED;
38+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
39+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
40+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
41+
42+
/**
43+
* A common base for OpenAPI 3.1 tests which provides the necessary foundation for OpenAPI 3.1 tests,
44+
* making the test setup cleaner and more consistent.
45+
*
46+
* @author zdary
47+
*/
48+
@SpringBootTest
49+
@TestPropertySource(properties = { SPRINGDOC_CACHE_DISABLED + "=true", "springdoc.api-docs.version=OPENAPI_3_1" })
50+
public abstract class AbstractSpringDocV31Test extends AbstractCommonTest {
51+
52+
@Test
53+
protected void testApp() throws Exception {
54+
String className = getClass().getSimpleName();
55+
String testNumber = className.replaceAll("[^0-9]", "");
56+
MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)).andExpect(status().isOk())
57+
.andExpect(jsonPath("$.openapi", is("3.1.0"))).andReturn();
58+
String result = mockMvcResult.getResponse().getContentAsString();
59+
String expected = getContent("results/3.1.0/app" + testNumber + ".json");
60+
assertEquals(expected, result, true);
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package test.org.springdoc.api.v31.app246;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Webhook;
5+
import io.swagger.v3.oas.annotations.Webhooks;
6+
import io.swagger.v3.oas.annotations.media.Content;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import io.swagger.v3.oas.annotations.parameters.RequestBody;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
10+
11+
import org.springframework.stereotype.Component;
12+
13+
@Webhooks({
14+
@Webhook(
15+
name = "includedPet",
16+
operation = @Operation(
17+
operationId = "includedPet",
18+
requestBody = @RequestBody(
19+
description = "Information about a new pet in the system",
20+
content = {
21+
@Content(
22+
mediaType = "application/json",
23+
schema = @Schema(
24+
description = "Webhook Pet",
25+
implementation = IncludedWebHookResource.RequestDto.class
26+
)
27+
)
28+
}
29+
),
30+
method = "post",
31+
responses = @ApiResponse(
32+
responseCode = "200",
33+
description = "Return a 200 status to indicate that the data was received successfully"
34+
)
35+
)
36+
)
37+
})
38+
@Component
39+
public class IncludedWebHookResource {
40+
41+
@Webhook(
42+
name = "includedNewPet",
43+
operation = @Operation(
44+
operationId = "includedNewPet",
45+
requestBody = @RequestBody(
46+
description = "Information about a new pet in the system",
47+
content = {
48+
@Content(
49+
mediaType = "application/json",
50+
schema = @Schema(
51+
description = "Webhook Pet",
52+
implementation = RequestDto.class
53+
)
54+
)
55+
}
56+
),
57+
method = "post",
58+
responses = @ApiResponse(
59+
responseCode = "200",
60+
description = "Return a 200 status to indicate that the data was received successfully"
61+
)
62+
)
63+
)
64+
public void includedNewPet(RequestDto requestDto) {
65+
// This method is intentionally left empty.
66+
// The actual processing of the webhook data would be implemented here.
67+
System.out.println("Received new pet with personal number: " + requestDto.getPersonalNumber());
68+
}
69+
70+
public static class RequestDto {
71+
72+
private String personalNumber;
73+
74+
public String getPersonalNumber() {
75+
return personalNumber;
76+
}
77+
78+
public void setPersonalNumber(String personalNumber) {
79+
this.personalNumber = personalNumber;
80+
}
81+
}
82+
}
83+
84+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package test.org.springdoc.api.v31.app246;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springdoc.core.utils.Constants;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.test.context.TestPropertySource;
8+
import test.org.springdoc.api.v31.AbstractSpringDocV31Test;
9+
10+
import static org.hamcrest.Matchers.is;
11+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
12+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
13+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
14+
15+
16+
/**
17+
* This test class verifies the webhook filtering functionality based on package scanning.
18+
* It ensures that only webhooks from the packages defined in {@code springdoc.packages-to-scan}
19+
* are included in the OpenAPI specification, and webhooks from packages in
20+
* {@code springdoc.packages-to-exclude} are correctly omitted.
21+
*/
22+
@SpringBootTest(classes = SpringDocApp246Test.SpringDocApp246.class)
23+
@TestPropertySource(properties = {
24+
"springdoc.packages-to-scan=test.org.springdoc.api.v31.app246",
25+
"springdoc.packages-to-exclude=test.org.springdoc.api.v31.app246.excluded",
26+
"springdoc.api-docs.version=OPENAPI_3_1"
27+
})
28+
public class SpringDocApp246Test extends AbstractSpringDocV31Test {
29+
30+
@SpringBootApplication
31+
static class SpringDocApp246 {
32+
}
33+
34+
@Test
35+
public void testApp2() throws Exception {
36+
mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL))
37+
.andExpect(status().isOk())
38+
.andExpect(jsonPath("$.webhooks.includedPet.post.requestBody.description", is("Information about a new pet in the system")))
39+
.andExpect(jsonPath("$.webhooks.includedNewPet.post.requestBody.description", is("Information about a new pet in the system")))
40+
.andExpect(jsonPath("$.webhooks.excludedNewPet").doesNotExist());
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package test.org.springdoc.api.v31.app246.excluded;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Webhook;
5+
6+
import io.swagger.v3.oas.annotations.Webhooks;
7+
import org.springframework.stereotype.Component;
8+
import test.org.springdoc.api.v31.app246.IncludedWebHookResource;
9+
10+
@Component
11+
@Webhooks({
12+
@Webhook(
13+
name = "excludedNewPet",
14+
operation = @Operation(
15+
operationId = "excludedNewPet",
16+
method = "post",
17+
summary = "This webhook should be ignored"
18+
)
19+
)
20+
})
21+
public class ExcludedWebHookResource {
22+
public void excludedNewPet(IncludedWebHookResource.RequestDto requestDto) {
23+
// This method is intentionally left empty.
24+
}
25+
}

0 commit comments

Comments
 (0)