Skip to content

Commit 8ad7477

Browse files
author
springdoc
committed
First support for Multiple OpenAPI definitions in one Spring Boot project #213
1 parent 521dba9 commit 8ad7477

33 files changed

+3424
-0
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ protected AbstractOpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequest
5353
this.openApiCustomisers = openApiCustomisers;
5454
}
5555

56+
protected AbstractOpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder,
57+
AbstractResponseBuilder responseBuilder, OperationBuilder operationParser,
58+
Optional<List<OpenApiCustomiser>> openApiCustomisers, List<String> pathsToMatch) {
59+
super();
60+
this.openAPIBuilder = openAPIBuilder;
61+
this.requestBuilder = requestBuilder;
62+
this.responseBuilder = responseBuilder;
63+
this.operationParser = operationParser;
64+
this.openApiCustomisers = openApiCustomisers;
65+
this.pathsToMatch=pathsToMatch;
66+
}
67+
5668
protected synchronized OpenAPI getOpenApi() {
5769
OpenAPI openApi;
5870
if (!computeDone) {

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

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public final class Constants {
99
public static final String API_DOCS_URL = "${springdoc.api-docs.path:#{T(org.springdoc.core.Constants).DEFAULT_API_DOCS_URL}}";
1010
public static final String DEFAULT_API_DOCS_URL_YAML = API_DOCS_URL + ".yaml";
1111
public static final String SPRINGDOC_ENABLED = "springdoc.api-docs.enabled";
12+
public static final String SPRINGDOC_GROUPS_ENABLED = "springdoc.api-docs.groups.enabled";
1213
public static final String SPRINGDOC_SWAGGER_UI_ENABLED = "springdoc.swagger-ui.enabled";
1314
public static final String SPRINGDOC_SHOW_ACTUATOR = "springdoc.show.actuator";
1415
public static final String SPRINGDOC_SHOW_ACTUATOR_VALUE = "${" + SPRINGDOC_SHOW_ACTUATOR + ":false}";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.springdoc.core;
2+
3+
import org.springdoc.api.OpenApiCustomiser;
4+
5+
import java.util.ArrayList;
6+
import java.util.Arrays;
7+
import java.util.List;
8+
import java.util.Objects;
9+
10+
public class GroupedOpenApi {
11+
12+
private final String group;
13+
private final List<OpenApiCustomiser> openApiCustomisers;
14+
private final List<String> pathsToMatch;
15+
16+
17+
private GroupedOpenApi(Builder builder) {
18+
this.group = Objects.requireNonNull(builder.group, "group");
19+
this.openApiCustomisers = Objects.requireNonNull(builder.openApiCustomisers);
20+
this.pathsToMatch = Objects.requireNonNull(builder.pathsToMatch);
21+
}
22+
23+
public static Builder builder() {
24+
return new Builder();
25+
}
26+
27+
public String getGroup() {
28+
return group;
29+
}
30+
31+
public List<String> getPathsToMatch() {
32+
return pathsToMatch;
33+
}
34+
35+
public List<OpenApiCustomiser> getOpenApiCustomisers() {
36+
return openApiCustomisers;
37+
}
38+
39+
public static class Builder {
40+
private String group;
41+
private List<String> pathsToMatch;
42+
private List<OpenApiCustomiser> openApiCustomisers = new ArrayList<>();
43+
44+
private Builder() {
45+
// use static factory method in parent class
46+
}
47+
48+
public Builder setGroup(String group) {
49+
this.group = group;
50+
return this;
51+
}
52+
53+
public Builder setPathsToMatch(String[] pathsToMatch) {
54+
this.pathsToMatch = Arrays.asList(pathsToMatch);
55+
return this;
56+
}
57+
58+
public Builder addOpenApiCustomiser(OpenApiCustomiser openApiCustomiser) {
59+
this.openApiCustomisers.add(openApiCustomiser);
60+
return this;
61+
}
62+
63+
public GroupedOpenApi build() {
64+
return new GroupedOpenApi(this);
65+
}
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.springdoc.core;
2+
3+
import io.swagger.v3.oas.models.OpenAPI;
4+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
import static org.springdoc.core.Constants.SPRINGDOC_GROUPS_ENABLED;
10+
11+
@Configuration
12+
@ConditionalOnProperty(name = SPRINGDOC_GROUPS_ENABLED, matchIfMissing = false)
13+
public class MultipleOpenApiSupportConfiguration {
14+
@Bean
15+
public BeanFactoryPostProcessor beanFactoryPostProcessor() {
16+
return beanFactory -> {
17+
for (String beanName : beanFactory.getBeanNamesForType(OpenAPI.class)) {
18+
beanFactory.getBeanDefinition(beanName).setScope("prototype");
19+
}
20+
for (String beanName : beanFactory.getBeanNamesForType(OpenAPIBuilder.class)) {
21+
beanFactory.getBeanDefinition(beanName).setScope("prototype");
22+
}
23+
};
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.ObjectFactory;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
9+
import org.springframework.http.MediaType;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RestController;
13+
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
14+
15+
import javax.servlet.http.HttpServletRequest;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.Optional;
19+
import java.util.stream.Collectors;
20+
21+
import static org.springdoc.core.Constants.*;
22+
23+
@RestController
24+
@ConditionalOnProperty(name = SPRINGDOC_GROUPS_ENABLED, matchIfMissing = false)
25+
public class MultipleOpenApiResource {
26+
27+
private final RequestMappingInfoHandlerMapping requestMappingHandlerMapping;
28+
29+
private final Optional<ActuatorProvider> servletContextProvider;
30+
31+
private final Map<String, OpenApiResource> groupedOpenApiResources;
32+
33+
@Value(SPRINGDOC_SHOW_ACTUATOR_VALUE)
34+
private boolean showActuator;
35+
36+
public MultipleOpenApiResource(List<GroupedOpenApi> groupedOpenApis,
37+
ObjectFactory<OpenAPIBuilder> defaultOpenAPIBuilder, OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder,
38+
AbstractResponseBuilder responseBuilder, OperationBuilder operationParser,
39+
RequestMappingInfoHandlerMapping requestMappingHandlerMapping, Optional<ActuatorProvider> servletContextProvider) {
40+
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
41+
this.servletContextProvider = servletContextProvider;
42+
this.groupedOpenApiResources = groupedOpenApis.stream()
43+
.collect(Collectors.toMap(GroupedOpenApi::getGroup, item ->
44+
new OpenApiResource(
45+
defaultOpenAPIBuilder.getObject(),
46+
requestBuilder,
47+
responseBuilder,
48+
operationParser,
49+
requestMappingHandlerMapping,
50+
servletContextProvider,
51+
Optional.of(item.getOpenApiCustomisers()), item.getPathsToMatch()
52+
)
53+
));
54+
}
55+
56+
@Operation(hidden = true)
57+
@GetMapping(value = API_DOCS_URL + "/{group}", produces = MediaType.APPLICATION_JSON_VALUE)
58+
public String openapiJson(HttpServletRequest request, @Value(API_DOCS_URL) String apiDocsUrl,
59+
@PathVariable String group)
60+
throws JsonProcessingException {
61+
return getOpenApiResourceOrThrow(group).openapiJson(request, apiDocsUrl + "/" + group);
62+
}
63+
64+
@Operation(hidden = true)
65+
@GetMapping(value = DEFAULT_API_DOCS_URL_YAML + "/{group}", produces = APPLICATION_OPENAPI_YAML)
66+
public String openapiYaml(HttpServletRequest request, @Value(DEFAULT_API_DOCS_URL_YAML) String apiDocsUrl,
67+
@PathVariable String group)
68+
throws JsonProcessingException {
69+
return getOpenApiResourceOrThrow(group).openapiYaml(request, apiDocsUrl + "/" + group);
70+
}
71+
72+
73+
private OpenApiResource getOpenApiResourceOrThrow(String group) {
74+
OpenApiResource openApiResource = groupedOpenApiResources.get(group);
75+
if (openApiResource == null) {
76+
throw new IllegalStateException("No OpenAPI resource found for group " + group);
77+
}
78+
return openApiResource;
79+
}
80+
}

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

+11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springdoc.core.AbstractResponseBuilder;
1313
import org.springdoc.core.OpenAPIBuilder;
1414
import org.springdoc.core.OperationBuilder;
15+
import org.springframework.beans.factory.annotation.Autowired;
1516
import org.springframework.beans.factory.annotation.Value;
1617
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1718
import org.springframework.core.annotation.AnnotationUtils;
@@ -42,6 +43,7 @@ public class OpenApiResource extends AbstractOpenApiResource {
4243
@Value(SPRINGDOC_SHOW_ACTUATOR_VALUE)
4344
private boolean showActuator;
4445

46+
@Autowired
4547
public OpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder,
4648
AbstractResponseBuilder responseBuilder, OperationBuilder operationParser,
4749
RequestMappingInfoHandlerMapping requestMappingHandlerMapping, Optional<ActuatorProvider> servletContextProvider,
@@ -51,6 +53,15 @@ public OpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder req
5153
this.servletContextProvider = servletContextProvider;
5254
}
5355

56+
public OpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder,
57+
AbstractResponseBuilder responseBuilder, OperationBuilder operationParser,
58+
RequestMappingInfoHandlerMapping requestMappingHandlerMapping, Optional<ActuatorProvider> servletContextProvider,
59+
Optional<List<OpenApiCustomiser>> openApiCustomisers , List<String> pathsToMatch) {
60+
super(openAPIBuilder, requestBuilder, responseBuilder, operationParser, openApiCustomisers,pathsToMatch);
61+
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
62+
this.servletContextProvider = servletContextProvider;
63+
}
64+
5465
@Operation(hidden = true)
5566
@GetMapping(value = API_DOCS_URL, produces = MediaType.APPLICATION_JSON_VALUE)
5667
public String openapiJson(HttpServletRequest request, @Value(API_DOCS_URL) String apiDocsUrl)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package test.org.springdoc.api.app68;
2+
3+
import org.junit.Test;
4+
import org.junit.runner.RunWith;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springdoc.core.Constants;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.test.context.ActiveProfiles;
12+
import org.springframework.test.context.TestPropertySource;
13+
import org.springframework.test.context.junit4.SpringRunner;
14+
import org.springframework.test.web.servlet.MockMvc;
15+
import org.springframework.test.web.servlet.MvcResult;
16+
import test.org.springdoc.api.AbstractSpringDocTest;
17+
18+
import java.nio.file.Files;
19+
import java.nio.file.Path;
20+
import java.nio.file.Paths;
21+
22+
import static org.hamcrest.Matchers.is;
23+
import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
24+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
25+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
26+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
27+
28+
@RunWith(SpringRunner.class)
29+
@ActiveProfiles("test")
30+
@SpringBootTest
31+
@AutoConfigureMockMvc
32+
@TestPropertySource(properties ="springdoc.api-docs.groups.enabled=true")
33+
public class SpringDocApp68Test {
34+
35+
protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractSpringDocTest.class);
36+
37+
public static String className;
38+
39+
@Autowired
40+
protected MockMvc mockMvc;
41+
42+
@Test
43+
public void testApp1() throws Exception {
44+
className = getClass().getSimpleName();
45+
MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL+"/store")).andExpect(status().isOk())
46+
.andExpect(jsonPath("$.openapi", is("3.0.1"))).andReturn();
47+
String result = mockMvcResult.getResponse().getContentAsString();
48+
Path path = Paths.get(getClass().getClassLoader().getResource("results/app681.json").toURI());
49+
byte[] fileBytes = Files.readAllBytes(path);
50+
String expected = new String(fileBytes);
51+
assertEquals(expected, result, true);
52+
}
53+
54+
@Test
55+
public void testApp2() throws Exception {
56+
className = getClass().getSimpleName();
57+
MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL+"/others")).andExpect(status().isOk())
58+
.andExpect(jsonPath("$.openapi", is("3.0.1"))).andReturn();
59+
String result = mockMvcResult.getResponse().getContentAsString();
60+
Path path = Paths.get(getClass().getClassLoader().getResource("results/app682.json").toURI());
61+
byte[] fileBytes = Files.readAllBytes(path);
62+
String expected = new String(fileBytes);
63+
assertEquals(expected, result, true);
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package test.org.springdoc.api.app68;
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+
13+
@SpringBootApplication
14+
public class SpringDocTestApp {
15+
public static void main(String[] args) {
16+
SpringApplication.run(SpringDocTestApp.class, args);
17+
}
18+
19+
@Bean
20+
public GroupedOpenApi storeOpenApi() {
21+
String paths[] = {"/store/**"};
22+
return GroupedOpenApi.builder().setGroup("store").setPathsToMatch(paths)
23+
.build();
24+
}
25+
26+
@Bean
27+
public GroupedOpenApi userOpenApi() {
28+
String paths[] = {"/user/**", "/pet/**"};
29+
return GroupedOpenApi.builder().setGroup("others").setPathsToMatch(paths)
30+
.build();
31+
}
32+
33+
34+
@Bean
35+
public OpenAPI customOpenAPI() {
36+
return new OpenAPI()
37+
.components(new Components().addSecuritySchemes("basicScheme",
38+
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("basic")))
39+
.info(new Info().title("Petstore API").version("v0").description(
40+
"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.")
41+
.termsOfService("http://swagger.io/terms/")
42+
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package test.org.springdoc.api.app68.api;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.web.context.request.NativeWebRequest;
5+
import org.springframework.web.server.ResponseStatusException;
6+
7+
import javax.servlet.http.HttpServletResponse;
8+
import java.io.IOException;
9+
10+
public class ApiUtil {
11+
12+
public static void setExampleResponse(NativeWebRequest req, String contentType, String example) {
13+
try {
14+
req.getNativeResponse(HttpServletResponse.class).addHeader("Content-Type", contentType);
15+
req.getNativeResponse(HttpServletResponse.class).getOutputStream().print(example);
16+
} catch (IOException e) {
17+
throw new RuntimeException(e);
18+
}
19+
}
20+
21+
public static void checkApiKey(NativeWebRequest req) {
22+
if (!"1".equals(System.getenv("DISABLE_API_KEY")) && !"special-key".equals(req.getHeader("api_key"))) {
23+
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing API key!");
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)