Skip to content

Commit cf05f3b

Browse files
author
bnasslahsen
committed
Added support for multiple OpenAPI definitions in spring webflux. Fixes #329
1 parent f13355c commit cf05f3b

File tree

29 files changed

+509
-19
lines changed

29 files changed

+509
-19
lines changed

pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<!--suppress ALL -->
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
34
<modelVersion>4.0.0</modelVersion>
45
<groupId>org.springdoc</groupId>
56
<artifactId>springdoc-openapi</artifactId>

springdoc-openapi-common/pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
1+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23
<modelVersion>4.0.0</modelVersion>
34
<artifactId>springdoc-openapi-common</artifactId>
45
<name>${project.artifactId}</name>

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public final class Constants {
2020
public static final String DEFAULT_WEB_JARS_PREFIX_URL = "/webjars";
2121
public static final String WEB_JARS_PREFIX_URL = "${springdoc.webjars.prefix:#{T(org.springdoc.core.Constants).DEFAULT_WEB_JARS_PREFIX_URL}}";
2222
public static final String SWAGGER_UI_URL = "/swagger-ui/index.html";
23-
public static final String SWAGGER_UI_OAUTH_REDIRECT_URL = "/swagger-ui/oauth2-redirect.html";
23+
public static final String SWAGGER_UI_OAUTH_REDIRECT_URL = "/swagger-ui/oauth2-redirect.html";
2424
public static final String APPLICATION_OPENAPI_YAML = "application/vnd.oai.openapi";
2525
public static final String DEFAULT_SWAGGER_UI_PATH = DEFAULT_PATH_SEPARATOR + "swagger-ui.html";
2626
public static final String SWAGGER_UI_PATH = "${springdoc.swagger-ui.path:#{T(org.springdoc.core.Constants).DEFAULT_SWAGGER_UI_PATH}}";

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public class SwaggerUiConfigProperties {
110110
/**
111111
* OAuth redirect URL.
112112
*/
113-
private String oauth2RedirectUrl=SWAGGER_UI_OAUTH_REDIRECT_URL;
113+
private String oauth2RedirectUrl = SWAGGER_UI_OAUTH_REDIRECT_URL;
114114
private String url;
115115

116116
public static void addGroup(String group) {

springdoc-openapi-data-rest/pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
34
<parent>
45
<artifactId>springdoc-openapi</artifactId>
56
<groupId>org.springdoc</groupId>

springdoc-openapi-kotlin/pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
1+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23
<modelVersion>4.0.0</modelVersion>
34
<artifactId>springdoc-openapi-kotlin</artifactId>
45
<name>${project.artifactId}</name>

springdoc-openapi-security/pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
34
<parent>
45
<artifactId>springdoc-openapi</artifactId>
56
<groupId>org.springdoc</groupId>

springdoc-openapi-ui/pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
1+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23
<modelVersion>4.0.0</modelVersion>
34
<artifactId>springdoc-openapi-ui</artifactId>
45
<name>${project.artifactId}</name>

springdoc-openapi-ui/src/main/java/org/springdoc/ui/SwaggerWelcome.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,6 @@ private void calculateUiRootPath() {
9797
sbUrl.append(mvcServletPath);
9898
if (swaggerPath.contains(DEFAULT_PATH_SEPARATOR))
9999
sbUrl.append(swaggerPath.substring(0, swaggerPath.lastIndexOf(DEFAULT_PATH_SEPARATOR)));
100-
this.uiRootPath=sbUrl.toString();
100+
this.uiRootPath = sbUrl.toString();
101101
}
102102
}

springdoc-openapi-ui/src/test/java/test/org/springdoc/ui/app5/SpringDocOauthContextPathTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
1010
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
1111

12-
@SpringBootTest(properties ="server.servlet.context-path=/context-path")
12+
@SpringBootTest(properties = "server.servlet.context-path=/context-path")
1313
public class SpringDocOauthContextPathTest extends AbstractSpringDocTest {
1414

1515
@Test

springdoc-openapi-webflux-core/pom.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
1+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23
<modelVersion>4.0.0</modelVersion>
34
<artifactId>springdoc-openapi-webflux-core</artifactId>
45
<name>${project.artifactId}</name>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.springdoc.api;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import io.swagger.v3.oas.annotations.Operation;
5+
import org.springdoc.core.*;
6+
import org.springframework.beans.factory.InitializingBean;
7+
import org.springframework.beans.factory.ObjectFactory;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.http.MediaType;
10+
import org.springframework.http.server.reactive.ServerHttpRequest;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.PathVariable;
13+
import org.springframework.web.bind.annotation.RestController;
14+
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
15+
import reactor.core.publisher.Mono;
16+
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Optional;
20+
import java.util.stream.Collectors;
21+
22+
import static org.springdoc.core.Constants.*;
23+
import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR;
24+
25+
@RestController
26+
public class MultipleOpenApiResource implements InitializingBean {
27+
28+
private final List<GroupedOpenApi> groupedOpenApis;
29+
private final ObjectFactory<OpenAPIBuilder> defaultOpenAPIBuilder;
30+
private final AbstractRequestBuilder requestBuilder;
31+
private final AbstractResponseBuilder responseBuilder;
32+
private final OperationBuilder operationParser;
33+
private final RequestMappingInfoHandlerMapping requestMappingHandlerMapping;
34+
private Map<String, OpenApiResource> groupedOpenApiResources;
35+
36+
public MultipleOpenApiResource(List<GroupedOpenApi> groupedOpenApis,
37+
ObjectFactory<OpenAPIBuilder> defaultOpenAPIBuilder, AbstractRequestBuilder requestBuilder,
38+
AbstractResponseBuilder responseBuilder, OperationBuilder operationParser,
39+
RequestMappingInfoHandlerMapping requestMappingHandlerMapping) {
40+
41+
this.groupedOpenApis = groupedOpenApis;
42+
this.defaultOpenAPIBuilder = defaultOpenAPIBuilder;
43+
this.requestBuilder = requestBuilder;
44+
this.responseBuilder = responseBuilder;
45+
this.operationParser = operationParser;
46+
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
47+
}
48+
49+
@Override
50+
public void afterPropertiesSet() throws Exception {
51+
this.groupedOpenApiResources = groupedOpenApis.stream()
52+
.collect(Collectors.toMap(GroupedOpenApi::getGroup, item ->
53+
new OpenApiResource(
54+
defaultOpenAPIBuilder.getObject(),
55+
requestBuilder,
56+
responseBuilder,
57+
operationParser,
58+
requestMappingHandlerMapping,
59+
Optional.of(item.getOpenApiCustomisers()), item.getPathsToMatch(), item.getPackagesToScan()
60+
)
61+
));
62+
}
63+
64+
@Operation(hidden = true)
65+
@GetMapping(value = API_DOCS_URL + "/{group}", produces = MediaType.APPLICATION_JSON_VALUE)
66+
public Mono<String> openapiJson(ServerHttpRequest serverHttpRequest, @Value(API_DOCS_URL) String apiDocsUrl, @PathVariable String group)
67+
throws JsonProcessingException {
68+
return getOpenApiResourceOrThrow(group).openapiJson(serverHttpRequest, apiDocsUrl + DEFAULT_PATH_SEPARATOR + group);
69+
}
70+
71+
@Operation(hidden = true)
72+
@GetMapping(value = DEFAULT_API_DOCS_URL_YAML + "/{group}", produces = APPLICATION_OPENAPI_YAML)
73+
public Mono<String> openapiYaml(ServerHttpRequest serverHttpRequest,
74+
@Value(DEFAULT_API_DOCS_URL_YAML) String apiDocsUrl, @PathVariable String group) throws JsonProcessingException {
75+
return getOpenApiResourceOrThrow(group).openapiYaml(serverHttpRequest, apiDocsUrl + DEFAULT_PATH_SEPARATOR + group);
76+
}
77+
78+
private OpenApiResource getOpenApiResourceOrThrow(String group) {
79+
OpenApiResource openApiResource = groupedOpenApiResources.get(group);
80+
if (openApiResource == null) {
81+
throw new IllegalStateException("No OpenAPI resource found for group " + group);
82+
}
83+
return openApiResource;
84+
}
85+
}

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

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ public OpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder req
4242
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
4343
}
4444

45+
public OpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder,
46+
AbstractResponseBuilder responseBuilder, OperationBuilder operationParser,
47+
RequestMappingInfoHandlerMapping requestMappingHandlerMapping,
48+
Optional<List<OpenApiCustomiser>> openApiCustomisers, List<String> pathsToMatch, List<String> packagesToScan) {
49+
super(openAPIBuilder, requestBuilder, responseBuilder, operationParser, openApiCustomisers, pathsToMatch, packagesToScan);
50+
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
51+
}
52+
4553
@Operation(hidden = true)
4654
@GetMapping(value = API_DOCS_URL, produces = MediaType.APPLICATION_JSON_VALUE)
4755
public Mono<String> openapiJson(ServerHttpRequest serverHttpRequest, @Value(API_DOCS_URL) String apiDocsUrl)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.springdoc.core;
2+
3+
import io.swagger.v3.oas.models.OpenAPI;
4+
import org.springdoc.api.MultipleOpenApiResource;
5+
import org.springframework.beans.factory.ObjectFactory;
6+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
7+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
8+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
9+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
13+
14+
import java.util.List;
15+
16+
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
17+
18+
19+
@Configuration
20+
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
21+
@ConditionalOnBean(GroupedOpenApi.class)
22+
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
23+
public class MultipleOpenApiSupportConfiguration {
24+
25+
@Bean
26+
public BeanFactoryPostProcessor beanFactoryPostProcessor() {
27+
return beanFactory -> {
28+
for (String beanName : beanFactory.getBeanNamesForType(OpenAPI.class)) {
29+
beanFactory.getBeanDefinition(beanName).setScope("prototype");
30+
}
31+
for (String beanName : beanFactory.getBeanNamesForType(OpenAPIBuilder.class)) {
32+
beanFactory.getBeanDefinition(beanName).setScope("prototype");
33+
}
34+
};
35+
}
36+
37+
@Bean
38+
public MultipleOpenApiResource multipleOpenApiResource(List<GroupedOpenApi> groupedOpenApis,
39+
ObjectFactory<OpenAPIBuilder> defaultOpenAPIBuilder, AbstractRequestBuilder requestBuilder,
40+
AbstractResponseBuilder responseBuilder, OperationBuilder operationParser,
41+
RequestMappingInfoHandlerMapping requestMappingHandlerMapping) {
42+
return new MultipleOpenApiResource(groupedOpenApis,
43+
defaultOpenAPIBuilder, requestBuilder,
44+
responseBuilder, operationParser,
45+
requestMappingHandlerMapping);
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2-
org.springdoc.core.SpringDocWebFluxConfiguration
2+
org.springdoc.core.SpringDocWebFluxConfiguration,\
3+
org.springdoc.core.MultipleOpenApiSupportConfiguration

springdoc-openapi-webflux-core/src/test/java/test/org/springdoc/api/AbstractSpringDocTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@
2323
@ActiveProfiles("test")
2424
public abstract class AbstractSpringDocTest {
2525

26+
protected String groupName = "";
2627
@Autowired
2728
private WebTestClient webTestClient;
28-
2929
@Autowired
3030
private ObjectMapper objectMapper;
3131

3232
@Test
3333
public void testApp() throws Exception {
34-
EntityExchangeResult<byte[]> getResult = webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL).exchange()
34+
EntityExchangeResult<byte[]> getResult = webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL + groupName).exchange()
3535
.expectStatus().isOk().expectBody().returnResult();
3636

3737
String result = new String(getResult.getResponseBody());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package test.org.springdoc.api.app66;
2+
3+
import test.org.springdoc.api.AbstractSpringDocTest;
4+
5+
public class SpringDocApp66Test extends AbstractSpringDocTest {
6+
7+
public SpringDocApp66Test() {
8+
this.groupName = "/stream";
9+
}
10+
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package test.org.springdoc.api.app66;
2+
3+
import io.swagger.v3.oas.models.Components;
4+
import io.swagger.v3.oas.models.OpenAPI;
5+
import io.swagger.v3.oas.models.info.Info;
6+
import io.swagger.v3.oas.models.info.License;
7+
import io.swagger.v3.oas.models.security.SecurityScheme;
8+
import org.springdoc.core.GroupedOpenApi;
9+
import org.springframework.boot.SpringApplication;
10+
import org.springframework.boot.autoconfigure.SpringBootApplication;
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.context.annotation.ComponentScan;
13+
14+
@SpringBootApplication
15+
@ComponentScan(basePackages = {"org.springdoc", "test.org.springdoc.api.app66"})
16+
public class SpringDocTestApp {
17+
public static void main(String[] args) {
18+
SpringApplication.run(SpringDocTestApp.class, args);
19+
}
20+
21+
@Bean
22+
public OpenAPI customOpenAPI() {
23+
return new OpenAPI()
24+
.components(new Components().addSecuritySchemes("basicScheme",
25+
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("basic")))
26+
.info(new Info().title("Tweet API").version("v0")
27+
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
28+
}
29+
30+
@Bean
31+
public GroupedOpenApi stramOpenApi() {
32+
String[] paths = {"/stream/**"};
33+
String[] packagedToMatch = {"test.org.springdoc.api.app66"};
34+
return GroupedOpenApi.builder().setGroup("stream").pathsToMatch(paths)
35+
.build();
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package test.org.springdoc.api.app66.controller;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.ExceptionHandler;
6+
import org.springframework.web.bind.annotation.ResponseStatus;
7+
import org.springframework.web.bind.annotation.RestControllerAdvice;
8+
import test.org.springdoc.api.app66.exception.TweetConflictException;
9+
import test.org.springdoc.api.app66.exception.TweetNotFoundException;
10+
import test.org.springdoc.api.app66.payload.ErrorResponse;
11+
12+
@RestControllerAdvice
13+
public class ExceptionTranslator {
14+
15+
16+
@SuppressWarnings("rawtypes")
17+
@ExceptionHandler(TweetConflictException.class)
18+
@ResponseStatus(HttpStatus.CONFLICT)
19+
public ResponseEntity handleDuplicateKeyException(TweetConflictException ex) {
20+
return ResponseEntity.status(HttpStatus.CONFLICT)
21+
.body(new ErrorResponse("A Tweet with the same text already exists"));
22+
}
23+
24+
@SuppressWarnings("rawtypes")
25+
@ExceptionHandler(TweetNotFoundException.class)
26+
@ResponseStatus(HttpStatus.NOT_FOUND)
27+
public ResponseEntity handleTweetNotFoundException(TweetNotFoundException ex) {
28+
return ResponseEntity.notFound().build();
29+
}
30+
31+
}

0 commit comments

Comments
 (0)