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 4275d353b..55dec9c1f 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 @@ -114,11 +114,7 @@ protected synchronized OpenAPI getOpenApi() { if (!computeDone || springDocConfigProperties.getCache().isDisabled()) { Instant start = Instant.now(); openAPIBuilder.build(); - Map restControllersMap = openAPIBuilder.getRestControllersMap(); - Map requestMappingMap = openAPIBuilder.getRequestMappingMap(); - Map controllerMap = openAPIBuilder.getControllersMap(); - Map restControllers = Stream.of(restControllersMap, requestMappingMap, controllerMap) - .flatMap(mapEl -> mapEl.entrySet().stream()) + Map mappingsMap = openAPIBuilder.getMappingsMap().entrySet().stream() .filter(controller -> (AnnotationUtils.findAnnotation(controller.getValue().getClass(), Hidden.class) == null)) .filter(controller -> !isHiddenRestControllers(controller.getValue().getClass())) @@ -128,7 +124,7 @@ protected synchronized OpenAPI getOpenApi() { // calculate generic responses responseBuilder.buildGenericResponse(openAPIBuilder.getComponents(), findControllerAdvice); - getPaths(restControllers); + getPaths(mappingsMap); openApi = openAPIBuilder.getCalculatedOpenAPI(); // run the optional customisers diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIBuilder.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIBuilder.java index 8b36c76c2..13afae9e2 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIBuilder.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIBuilder.java @@ -49,6 +49,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springdoc.core.customizers.OpenApiBuilderCustomiser; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.context.ApplicationContext; @@ -81,10 +82,14 @@ public class OpenAPIBuilder { private final SecurityParser securityParser; + private final Map mappingsMap = new HashMap<>(); + private final Map springdocTags = new HashMap<>(); private final Optional springSecurityOAuth2Provider; + private final List openApiBuilderCustomisers; + private boolean isServersPresent; private String serverBaseUrl; @@ -92,7 +97,9 @@ public class OpenAPIBuilder { private final SpringDocConfigProperties springDocConfigProperties; @SuppressWarnings("WeakerAccess") - OpenAPIBuilder(Optional openAPI, ApplicationContext context, SecurityParser securityParser, Optional springSecurityOAuth2Provider, SpringDocConfigProperties springDocConfigProperties) { + OpenAPIBuilder(Optional openAPI, ApplicationContext context, SecurityParser securityParser, + Optional springSecurityOAuth2Provider, SpringDocConfigProperties springDocConfigProperties, + List openApiBuilderCustomisers) { if (openAPI.isPresent()) { this.openAPI = openAPI.get(); if (this.openAPI.getComponents() == null) @@ -106,6 +113,7 @@ public class OpenAPIBuilder { this.securityParser = securityParser; this.springSecurityOAuth2Provider = springSecurityOAuth2Provider; this.springDocConfigProperties = springDocConfigProperties; + this.openApiBuilderCustomisers = openApiBuilderCustomisers; } private static String splitCamelCase(String str) { @@ -146,12 +154,18 @@ else if (calculatedOpenAPI.getInfo() == null) { Info infos = new Info().title(DEFAULT_TITLE).version(DEFAULT_VERSION); calculatedOpenAPI.setInfo(infos); } + // Set default mappings + this.mappingsMap.putAll(context.getBeansWithAnnotation(RestController.class)); + this.mappingsMap.putAll(context.getBeansWithAnnotation(RequestMapping.class)); + this.mappingsMap.putAll(context.getBeansWithAnnotation(Controller.class)); + // default server value if (CollectionUtils.isEmpty(calculatedOpenAPI.getServers()) || !isServersPresent) { this.updateServers(calculatedOpenAPI); } // add security schemes this.calculateSecuritySchemes(calculatedOpenAPI.getComponents()); + Optional.ofNullable(this.openApiBuilderCustomisers).ifPresent(customisers -> customisers.forEach(customiser -> customiser.customise(this))); } public void updateServers(OpenAPI openAPI) { @@ -397,16 +411,12 @@ public void addTag(Set handlerMethods, io.swagger.v3.oas.models.t handlerMethods.forEach(handlerMethod -> springdocTags.put(handlerMethod, tag)); } - public Map getRestControllersMap() { - return context.getBeansWithAnnotation(RestController.class); - } - - public Map getRequestMappingMap() { - return context.getBeansWithAnnotation(RequestMapping.class); + public Map getMappingsMap() { + return this.mappingsMap; } - public Map getControllersMap() { - return context.getBeansWithAnnotation(Controller.class); + public void addMappings(Map mappings) { + this.mappingsMap.putAll(mappings); } public Map getControllerAdviceMap() { diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java index 22ff8ea03..ce9563ae4 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java @@ -30,6 +30,7 @@ import org.springdoc.core.converters.ModelConverterRegistrar; import org.springdoc.core.converters.PropertyCustomizingConverter; import org.springdoc.core.converters.ResponseSupportConverter; +import org.springdoc.core.customizers.OpenApiBuilderCustomiser; import org.springdoc.core.customizers.PropertyCustomizer; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; @@ -82,8 +83,10 @@ IgnoredParameterAnnotationsDefault ignoredParameterAnnotationsDefault() { } @Bean - public OpenAPIBuilder openAPIBuilder(Optional openAPI, ApplicationContext context, SecurityParser securityParser, Optional springSecurityOAuth2Provider,SpringDocConfigProperties springDocConfigProperties) { - return new OpenAPIBuilder(openAPI, context, securityParser, springSecurityOAuth2Provider,springDocConfigProperties); + public OpenAPIBuilder openAPIBuilder(Optional openAPI, ApplicationContext context, SecurityParser securityParser, + Optional springSecurityOAuth2Provider, SpringDocConfigProperties springDocConfigProperties, + List openApiBuilderCustomisers) { + return new OpenAPIBuilder(openAPI, context, securityParser, springSecurityOAuth2Provider,springDocConfigProperties, openApiBuilderCustomisers); } @Bean diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/customizers/OpenApiBuilderCustomiser.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/customizers/OpenApiBuilderCustomiser.java new file mode 100644 index 000000000..c8a664348 --- /dev/null +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/customizers/OpenApiBuilderCustomiser.java @@ -0,0 +1,25 @@ +/* + * + * * 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.customizers; + +import org.springdoc.core.OpenAPIBuilder; + +public interface OpenApiBuilderCustomiser { + void customise(OpenAPIBuilder openApiBuilder); +} diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/AbstractSpringDocTest.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/AbstractSpringDocTest.java index 23b3d3441..eaba438b2 100644 --- a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/AbstractSpringDocTest.java +++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/AbstractSpringDocTest.java @@ -47,32 +47,31 @@ @AutoConfigureMockMvc public abstract class AbstractSpringDocTest { - protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractSpringDocTest.class); + protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractSpringDocTest.class); - public static String className; + public static String className; - @Autowired - protected MockMvc mockMvc; + @Autowired + protected MockMvc mockMvc; - public static String getContent(String fileName) throws Exception { - try { - Path path = Paths.get(FileUtils.class.getClassLoader().getResource(fileName).toURI()); - byte[] fileBytes = Files.readAllBytes(path); - return new String(fileBytes, StandardCharsets.UTF_8); - } - catch (Exception e) { - throw new RuntimeException("Failed to read file: " + fileName, e); - } - } + public static String getContent(String fileName) throws Exception { + try { + Path path = Paths.get(FileUtils.class.getClassLoader().getResource(fileName).toURI()); + byte[] fileBytes = Files.readAllBytes(path); + return new String(fileBytes, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException("Failed to read file: " + fileName, e); + } + } - @Test - public void testApp() throws Exception { - className = getClass().getSimpleName(); - String testNumber = className.replaceAll("[^0-9]", ""); - MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)).andExpect(status().isOk()) - .andExpect(jsonPath("$.openapi", is("3.0.1"))).andReturn(); - String result = mockMvcResult.getResponse().getContentAsString(); - String expected = getContent("results/app" + testNumber + ".json"); - assertEquals(expected, result, true); - } + @Test + public void testApp() throws Exception { + className = getClass().getSimpleName(); + String testNumber = className.replaceAll("[^0-9]", ""); + MvcResult mockMvcResult = mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)).andExpect(status().isOk()).andExpect(jsonPath("$.openapi", is("3.0.1"))) + .andReturn(); + String result = mockMvcResult.getResponse().getContentAsString(); + String expected = getContent("results/app" + testNumber + ".json"); + assertEquals(expected, result, true); + } } diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app92/SpringDocApp92Test.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app92/SpringDocApp92Test.java new file mode 100644 index 000000000..a944f41cd --- /dev/null +++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app92/SpringDocApp92Test.java @@ -0,0 +1,123 @@ +/* + * + * * 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.app92; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.RandomStringUtils; +import org.springdoc.api.ActuatorProvider; +import org.springdoc.api.OpenApiResource; +import org.springdoc.core.AbstractRequestBuilder; +import org.springdoc.core.GenericResponseBuilder; +import org.springdoc.core.OpenAPIBuilder; +import org.springdoc.core.OperationBuilder; +import org.springdoc.core.SpringDocConfigProperties; +import org.springdoc.core.customizers.OpenApiBuilderCustomiser; +import org.springdoc.core.customizers.OpenApiCustomiser; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import test.org.springdoc.api.AbstractSpringDocTest; +import test.org.springdoc.api.app91.Greeting; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.springdoc.core.Constants.DEFAULT_GROUP_NAME; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@TestPropertySource(properties = "springdoc.default-produces-media-type=application/json") +public class SpringDocApp92Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Bean + public GreetingController greetingController() { + return new GreetingController(); + } + + @Bean + public OpenApiBuilderCustomiser customOpenAPI() { + return openApiBuilder -> openApiBuilder.addMappings(Collections.singletonMap("greetingController", new GreetingController())); + } + + @Bean + public RequestMappingHandlerMapping defaultTestHandlerMapping(GreetingController greetingController) throws NoSuchMethodException { + RequestMappingHandlerMapping result = new RequestMappingHandlerMapping(); + RequestMappingInfo requestMappingInfo = + RequestMappingInfo.paths("/test").methods(RequestMethod.GET).produces(MediaType.APPLICATION_JSON_VALUE).build(); + + result.setApplicationContext(this.applicationContext); + result.registerMapping(requestMappingInfo, "greetingController", GreetingController.class.getDeclaredMethod("sayHello2")); + //result.handlerme + return result; + } + + @Bean(name = "mvcOpenApiResource") + public OpenApiResource openApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder, GenericResponseBuilder responseBuilder, + OperationBuilder operationParser, + @Qualifier("defaultTestHandlerMapping") RequestMappingHandlerMapping requestMappingHandlerMapping, + Optional servletContextProvider, SpringDocConfigProperties springDocConfigProperties, + Optional> openApiCustomisers) { + return new OpenApiResource(DEFAULT_GROUP_NAME, openAPIBuilder, requestBuilder, responseBuilder, operationParser, requestMappingHandlerMapping, + servletContextProvider, openApiCustomisers, springDocConfigProperties); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + } + + @ResponseBody + @Tag(name = "Demo", description = "The Demo API") + public static class GreetingController { + + @GetMapping(produces = APPLICATION_JSON_VALUE) + @Operation(summary = "This API will return a random greeting.") + public ResponseEntity sayHello() { + return ResponseEntity.ok(new Greeting(RandomStringUtils.randomAlphanumeric(10))); + } + + @GetMapping("/test") + @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "item created"), + @ApiResponse(responseCode = "400", description = "invalid input, object invalid"), + @ApiResponse(responseCode = "409", description = "an existing item already exists") }) + public ResponseEntity sayHello2() { + return ResponseEntity.ok(new Greeting(RandomStringUtils.randomAlphanumeric(10))); + } + + } +} diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app92.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app92.json new file mode 100644 index 000000000..140e54673 --- /dev/null +++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app92.json @@ -0,0 +1,77 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "Demo", + "description": "The Demo API" + } + ], + "paths": { + "/test": { + "get": { + "tags": [ + "Demo" + ], + "operationId": "sayHello2", + "responses": { + "400": { + "description": "invalid input, object invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Greeting" + } + } + } + }, + "201": { + "description": "item created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Greeting" + } + } + } + }, + "409": { + "description": "an existing item already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Greeting" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Greeting": { + "title": "Greeting", + "type": "object", + "properties": { + "payload": { + "type": "string", + "description": "The greeting value", + "example": "sdfsdfs" + } + }, + "description": "An object containing a greeting message" + } + } + } +}