1 /*
2  *
3  *  * Copyright 2019-2020 the original author or authors.
4  *  *
5  *  * Licensed under the Apache License, Version 2.0 (the "License");
6  *  * you may not use this file except in compliance with the License.
7  *  * You may obtain a copy of the License at
8  *  *
9  *  *      https://www.apache.org/licenses/LICENSE-2.0
10  *  *
11  *  * Unless required by applicable law or agreed to in writing, software
12  *  * distributed under the License is distributed on an "AS IS" BASIS,
13  *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  * See the License for the specific language governing permissions and
15  *  * limitations under the License.
16  *
17  */

18
19 package org.springdoc.webmvc.api;
20
21 import java.util.HashSet;
22 import java.util.LinkedHashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.Set;
27
28 import javax.servlet.http.HttpServletRequest;
29
30 import com.fasterxml.jackson.core.JsonProcessingException;
31 import io.swagger.v3.core.util.Json;
32 import io.swagger.v3.core.util.PathUtils;
33 import io.swagger.v3.core.util.Yaml;
34 import io.swagger.v3.oas.annotations.Operation;
35 import io.swagger.v3.oas.models.OpenAPI;
36 import org.springdoc.api.AbstractOpenApiResource;
37 import org.springdoc.core.AbstractRequestBuilder;
38 import org.springdoc.core.GenericResponseBuilder;
39 import org.springdoc.core.OpenAPIBuilder;
40 import org.springdoc.core.OperationBuilder;
41 import org.springdoc.core.SecurityOAuth2Provider;
42 import org.springdoc.core.SpringDocConfigProperties;
43 import org.springdoc.core.customizers.OpenApiCustomiser;
44
45 import org.springframework.beans.factory.annotation.Value;
46 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
47 import org.springframework.core.annotation.AnnotationUtils;
48 import org.springframework.http.MediaType;
49 import org.springframework.web.bind.annotation.GetMapping;
50 import org.springframework.web.bind.annotation.RequestMethod;
51 import org.springframework.web.bind.annotation.ResponseBody;
52 import org.springframework.web.bind.annotation.RestController;
53 import org.springframework.web.method.HandlerMethod;
54 import org.springframework.web.servlet.ModelAndView;
55 import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
56 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
57 import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
58
59 import static org.springdoc.core.Constants.API_DOCS_URL;
60 import static org.springdoc.core.Constants.APPLICATION_OPENAPI_YAML;
61 import static org.springdoc.core.Constants.DEFAULT_API_DOCS_URL_YAML;
62 import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR;
63
64 @RestController
65 @ConditionalOnMissingBean(name = "openApiResource")
66 public class OpenApiResource extends AbstractOpenApiResource {
67
68     private final RequestMappingInfoHandlerMapping requestMappingHandlerMapping;
69
70     private final Optional<ActuatorProvider> servletContextProvider;
71
72     private final Optional<SecurityOAuth2Provider> springSecurityOAuth2Provider;
73
74     public OpenApiResource(String groupName, OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder,
75             GenericResponseBuilder responseBuilder, OperationBuilder operationParser,
76             RequestMappingInfoHandlerMapping requestMappingHandlerMapping,
77             Optional<ActuatorProvider> servletContextProvider,
78             Optional<List<OpenApiCustomiser>> openApiCustomisers,
79             SpringDocConfigProperties springDocConfigProperties,
80             Optional<SecurityOAuth2Provider> springSecurityOAuth2Provider) {
81         super(groupName, openAPIBuilder, requestBuilder, responseBuilder, operationParser, openApiCustomisers, springDocConfigProperties);
82         this.requestMappingHandlerMapping = requestMappingHandlerMapping;
83         this.servletContextProvider = servletContextProvider;
84         this.springSecurityOAuth2Provider = springSecurityOAuth2Provider;
85     }
86
87     @Operation(hidden = true)
88     @GetMapping(value = API_DOCS_URL, produces = MediaType.APPLICATION_JSON_VALUE)
89     public String openapiJson(HttpServletRequest request, @Value(API_DOCS_URL) String apiDocsUrl)
90             throws JsonProcessingException {
91         calculateServerUrl(request, apiDocsUrl);
92         OpenAPI openAPI = this.getOpenApi();
93         return Json.mapper().writeValueAsString(openAPI);
94     }
95
96     @Operation(hidden = true)
97     @GetMapping(value = DEFAULT_API_DOCS_URL_YAML, produces = APPLICATION_OPENAPI_YAML)
98     public String openapiYaml(HttpServletRequest request, @Value(DEFAULT_API_DOCS_URL_YAML) String apiDocsUrl)
99             throws JsonProcessingException {
100         calculateServerUrl(request, apiDocsUrl);
101         OpenAPI openAPI = this.getOpenApi();
102         return Yaml.mapper().writeValueAsString(openAPI);
103     }
104
105     @Override
106     protected void getPaths(Map<String, Object> restControllers) {
107         Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
108         calculatePath(restControllers, map, Optional.empty());
109
110         if (servletContextProvider.isPresent()) {
111             map = servletContextProvider.get().getMethods();
112             this.openAPIBuilder.addTag(new HashSet<>(map.values()), servletContextProvider.get().getTag());
113             calculatePath(restControllers, map, servletContextProvider);
114         }
115         if (this.springSecurityOAuth2Provider.isPresent()) {
116             SecurityOAuth2Provider securityOAuth2Provider = this.springSecurityOAuth2Provider.get();
117             Map<RequestMappingInfo, HandlerMethod> mapOauth = securityOAuth2Provider.getHandlerMethods();
118             Map<String, Object> requestMappingMapSec = securityOAuth2Provider.getFrameworkEndpoints();
119             Class[] additionalRestClasses = requestMappingMapSec.values().stream().map(Object::getClass).toArray(Class[]::new);
120             AbstractOpenApiResource.addRestControllers(additionalRestClasses);
121             calculatePath(requestMappingMapSec, mapOauth, Optional.empty());
122         }
123     }
124
125     private void calculatePath(Map<String, Object> restControllers, Map<RequestMappingInfo, HandlerMethod> map, Optional<ActuatorProvider> actuatorProvider) {
126         for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
127             RequestMappingInfo requestMappingInfo = entry.getKey();
128             HandlerMethod handlerMethod = entry.getValue();
129             PatternsRequestCondition patternsRequestCondition = requestMappingInfo.getPatternsCondition();
130             Set<String> patterns = patternsRequestCondition.getPatterns();
131             Map<String, String> regexMap = new LinkedHashMap<>();
132             for (String pattern : patterns) {
133                 String operationPath = PathUtils.parsePath(pattern, regexMap);
134                 if (((actuatorProvider.isPresent() && actuatorProvider.get().isRestController(operationPath))
135                         || isRestController(restControllers, handlerMethod, operationPath))
136                         && isPackageToScan(handlerMethod.getBeanType().getPackage().getName())
137                         && isPathToMatch(operationPath)) {
138                     Set<RequestMethod> requestMethods = requestMappingInfo.getMethodsCondition().getMethods();
139                     // default allowed requestmethods
140                     if (requestMethods.isEmpty())
141                         requestMethods = this.getDefaultAllowedHttpMethods();
142                     calculatePath(handlerMethod, operationPath, requestMethods);
143                 }
144             }
145         }
146     }
147
148     private boolean isRestController(Map<String, Object> restControllers, HandlerMethod handlerMethod,
149             String operationPath) {
150         ResponseBody responseBodyAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class);
151         if (responseBodyAnnotation == null)
152             responseBodyAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class);
153
154         return (responseBodyAnnotation != null && restControllers.containsKey(handlerMethod.getBean().toString()) || isAdditionalRestController(handlerMethod.getBeanType()))
155                 && operationPath.startsWith(DEFAULT_PATH_SEPARATOR)
156                 && (springDocConfigProperties.isModelAndViewAllowed() || !ModelAndView.class.isAssignableFrom(handlerMethod.getMethod().getReturnType()));
157     }
158
159     private void calculateServerUrl(HttpServletRequest request, String apiDocsUrl) {
160         String requestUrl = decode(request.getRequestURL().toString());
161         String calculatedUrl = requestUrl.substring(0, requestUrl.length() - apiDocsUrl.length());
162         openAPIBuilder.setServerBaseUrl(calculatedUrl);
163     }
164 }
165