Skip to content

Commit 1371a29

Browse files
committed
Updated class and method javadoc handling
dto's class javadoc: added as description for the schema controller's class javadoc: added as description of the tag controller's method javadoc: first line as summary everything as description filters the generic responses by the declared exceptions: only if override-with-generic-response-if-declared is true @throws in method javadoc: overrides the @return of the ExceptionHandler's method only if override-with-generic-response-if-declared is true
1 parent 008aad6 commit 1371a29

31 files changed

+1516
-17
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
315315
Map<String, Object> findControllerAdvice = openAPIService.getControllerAdviceMap();
316316
// calculate generic responses
317317
openApi = openAPIService.getCalculatedOpenAPI();
318-
if (springDocConfigProperties.isOverrideWithGenericResponse()) {
318+
if (springDocConfigProperties.isOverrideWithGenericResponse() || springDocConfigProperties.isOverrideWithGenericResponseIfDeclared()) {
319319
if (!CollectionUtils.isEmpty(mappingsMap))
320320
findControllerAdvice.putAll(mappingsMap);
321321
responseBuilder.buildGenericResponse(openApi.getComponents(), findControllerAdvice, finalLocale);
@@ -463,8 +463,15 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
463463
// get javadoc method description
464464
if (javadocProvider != null) {
465465
String description = javadocProvider.getMethodJavadocDescription(handlerMethod.getMethod());
466-
if (!StringUtils.isEmpty(description) && StringUtils.isEmpty(operation.getDescription()))
466+
if (!StringUtils.isEmpty(description)
467+
&& StringUtils.isEmpty(operation.getDescription())) {
467468
operation.setDescription(description);
469+
}
470+
String summary = javadocProvider.getFirstSentence(description);
471+
if (!StringUtils.isEmpty(summary)
472+
&& StringUtils.isEmpty(operation.getSummary())) {
473+
operation.setSummary(javadocProvider.getFirstSentence(description));
474+
}
468475
}
469476

470477
Set<io.swagger.v3.oas.annotations.callbacks.Callback> apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.callbacks.Callback.class);

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222

2323
import java.lang.annotation.Annotation;
2424
import java.lang.reflect.Method;
25+
import java.lang.reflect.Parameter;
2526
import java.lang.reflect.ParameterizedType;
2627
import java.lang.reflect.Type;
2728
import java.util.ArrayList;
2829
import java.util.Arrays;
30+
import java.util.HashMap;
2931
import java.util.HashSet;
3032
import java.util.LinkedHashMap;
3133
import java.util.List;
@@ -64,6 +66,7 @@
6466
import org.springframework.web.method.ControllerAdviceBean;
6567
import org.springframework.web.method.HandlerMethod;
6668

69+
import static java.util.Arrays.asList;
6770
import static org.springdoc.core.Constants.DEFAULT_DESCRIPTION;
6871
import static org.springdoc.core.SpringDocAnnotationsUtils.extractSchema;
6972
import static org.springdoc.core.SpringDocAnnotationsUtils.getContent;
@@ -75,6 +78,11 @@
7578
* @author bnasslahsen
7679
*/
7780
public class GenericResponseService {
81+
/**
82+
* This extension name is used to temporary store
83+
* the exception classes.
84+
*/
85+
private static final String EXTENSION_EXCEPTION_CLASSES = "x-exception-class";
7886

7987
/**
8088
* The Operation builder.
@@ -135,7 +143,11 @@ public GenericResponseService(OperationService operationService, List<ReturnType
135143
*/
136144
public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
137145
MethodAttributes methodAttributes) {
138-
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(getGenericMapResponse(handlerMethod.getBeanType()));
146+
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod.getBeanType());
147+
if (springDocConfigProperties.isOverrideWithGenericResponseIfDeclared()) {
148+
genericMapResponse = filterAndEnrichGenericMapResponseByDeclarations(handlerMethod, genericMapResponse);
149+
}
150+
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(genericMapResponse);
139151
//Then use the apiResponses from documentation
140152
ApiResponses apiResponsesFromDoc = operation.getResponses();
141153
if (!CollectionUtils.isEmpty(apiResponsesFromDoc))
@@ -147,6 +159,41 @@ public ApiResponses build(Components components, HandlerMethod handlerMethod, Op
147159
return apiResponses;
148160
}
149161

162+
/**
163+
* Filters the generic API responses by the declared exceptions.
164+
* If Javadoc comment found for the declaration than it overrides the default description.
165+
*
166+
* @param handlerMethod the method which can have exception declarations
167+
* @param genericMapResponse the default generic API responses
168+
* @return the filtered and enriched responses
169+
*/
170+
private Map<String, ApiResponse> filterAndEnrichGenericMapResponseByDeclarations(HandlerMethod handlerMethod, Map<String, ApiResponse> genericMapResponse) {
171+
Map<String, ApiResponse> result = new HashMap<>();
172+
for (Map.Entry<String, ApiResponse> genericResponse : genericMapResponse.entrySet()) {
173+
Map<String, Object> extensions = genericResponse.getValue().getExtensions();
174+
Set<Class<?>> genericExceptions = (Set<Class<?>>) extensions.get(EXTENSION_EXCEPTION_CLASSES);
175+
for (Class<?> declaredException : handlerMethod.getMethod().getExceptionTypes()) {
176+
if (genericExceptions.contains(declaredException)) {
177+
ApiResponse clone = cloneApiResponse(genericResponse.getValue());
178+
clone.getExtensions().remove(EXTENSION_EXCEPTION_CLASSES);
179+
if (operationService.getJavadocProvider() != null) {
180+
JavadocProvider javadocProvider = operationService.getJavadocProvider();
181+
Map<String, String> javadocThrows = javadocProvider.getMethodJavadocThrows(handlerMethod.getMethod());
182+
String description = javadocThrows.get(declaredException.getName());
183+
if (description == null) {
184+
description = javadocThrows.get(declaredException.getSimpleName());
185+
}
186+
if (description != null && !description.trim().isEmpty()) {
187+
clone.setDescription(description);
188+
}
189+
}
190+
result.put(genericResponse.getKey(), clone);
191+
}
192+
}
193+
}
194+
return result;
195+
}
196+
150197
/**
151198
* Build generic response.
152199
*
@@ -504,6 +551,23 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent()))
504551
if (schemaN != null && ArrayUtils.isNotEmpty(methodAttributes.getMethodProduces()))
505552
Arrays.stream(methodAttributes.getMethodProduces()).forEach(mediaTypeStr -> mergeSchema(existingContent, schemaN, mediaTypeStr));
506553
}
554+
if (springDocConfigProperties.isOverrideWithGenericResponseIfDeclared()
555+
&& methodParameter.getExecutable().isAnnotationPresent(ExceptionHandler.class)) {
556+
// ExceptionHandler's exception class resolution is non-trivial
557+
// more info on its javadoc
558+
ExceptionHandler exceptionHandler = methodParameter.getExecutable().getAnnotation(ExceptionHandler.class);
559+
Set<Class<?>> exceptions = new HashSet<>();
560+
if (exceptionHandler.value().length == 0) {
561+
for (Parameter parameter : methodParameter.getExecutable().getParameters()) {
562+
if (Throwable.class.isAssignableFrom(parameter.getType())) {
563+
exceptions.add(parameter.getType());
564+
}
565+
}
566+
} else {
567+
exceptions.addAll(asList(exceptionHandler.value()));
568+
}
569+
apiResponse.addExtension(EXTENSION_EXCEPTION_CLASSES, exceptions);
570+
}
507571
apiResponsesOp.addApiResponse(httpCode, apiResponse);
508572
}
509573

@@ -622,4 +686,15 @@ private boolean isHttpCodePresent(String httpCode, Set<io.swagger.v3.oas.annotat
622686
public static void setResponseEntityExceptionHandlerClass(Class<?> responseEntityExceptionHandlerClass) {
623687
GenericResponseService.responseEntityExceptionHandlerClass = responseEntityExceptionHandlerClass;
624688
}
689+
690+
private ApiResponse cloneApiResponse(ApiResponse original) {
691+
ApiResponse clone = new ApiResponse();
692+
clone.set$ref(original.get$ref());
693+
clone.setDescription(original.getDescription());
694+
clone.setContent(original.getContent());
695+
clone.setHeaders(original.getHeaders() == null ? null : new HashMap<>(original.getHeaders()));
696+
clone.setExtensions(original.getExtensions() == null ? null : new HashMap<>(original.getExtensions()));
697+
clone.setLinks(original.getLinks() == null ? null : new HashMap<>(original.getLinks()));
698+
return clone;
699+
}
625700
}

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
import org.slf4j.LoggerFactory;
5858
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
5959
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
60+
import org.springdoc.core.providers.JavadocProvider;
61+
6062
import org.springframework.beans.BeansException;
6163
import org.springframework.beans.factory.config.BeanDefinition;
6264
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
@@ -154,6 +156,11 @@ public class OpenAPIService implements ApplicationContextAware {
154156
*/
155157
private PropertyResolverUtils propertyResolverUtils;
156158

159+
/**
160+
* The javadoc provider.
161+
*/
162+
private Optional<JavadocProvider> javadocProvider;
163+
157164
/**
158165
* The Basic error controller.
159166
*/
@@ -188,7 +195,8 @@ public class OpenAPIService implements ApplicationContextAware {
188195
public OpenAPIService(Optional<OpenAPI> openAPI, SecurityService securityParser,
189196
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
190197
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
191-
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers) {
198+
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
199+
Optional<JavadocProvider> javadocProvider) {
192200
if (openAPI.isPresent()) {
193201
this.openAPI = openAPI.get();
194202
if (this.openAPI.getComponents() == null)
@@ -203,6 +211,7 @@ public OpenAPIService(Optional<OpenAPI> openAPI, SecurityService securityParser,
203211
this.springDocConfigProperties = springDocConfigProperties;
204212
this.openApiBuilderCustomisers = openApiBuilderCustomizers;
205213
this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
214+
this.javadocProvider = javadocProvider;
206215
if (springDocConfigProperties.isUseFqn())
207216
TypeNameResolver.std.setUseFqn(true);
208217
}
@@ -347,8 +356,19 @@ public Operation buildTags(HandlerMethod handlerMethod, Operation operation, Ope
347356
}
348357
}
349358

350-
if (isAutoTagClasses(operation))
351-
operation.addTagsItem(splitCamelCase(handlerMethod.getBeanType().getSimpleName()));
359+
if (isAutoTagClasses(operation)) {
360+
String tagAutoName = splitCamelCase(handlerMethod.getBeanType().getSimpleName());
361+
operation.addTagsItem(tagAutoName);
362+
io.swagger.v3.oas.models.tags.Tag tag = new io.swagger.v3.oas.models.tags.Tag();
363+
tag.setName(tagAutoName);
364+
if (javadocProvider.isPresent()) {
365+
tag.setDescription(javadocProvider.get().getClassJavadoc(handlerMethod.getBeanType()));
366+
}
367+
if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
368+
openAPI.addTagsItem(tag);
369+
}
370+
371+
}
352372

353373
if (!CollectionUtils.isEmpty(tags)) {
354374
// Existing tags
@@ -732,7 +752,7 @@ public SecurityService getSecurityParser() {
732752

733753
/**
734754
* Gets server base URL
735-
*
755+
*
736756
* @return the server base URL
737757
*/
738758
public String getServerBaseUrl() {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ public class SpringDocConfigProperties {
117117
*/
118118
private boolean overrideWithGenericResponse = true;
119119

120+
/**
121+
* The Override with generic response only if the exception is declared
122+
*/
123+
private boolean overrideWithGenericResponseIfDeclared = false;
124+
120125
/**
121126
* The Remove broken reference definitions.
122127
*/
@@ -592,6 +597,24 @@ public void setOverrideWithGenericResponse(boolean overrideWithGenericResponse)
592597
this.overrideWithGenericResponse = overrideWithGenericResponse;
593598
}
594599

600+
/**
601+
* Is override with generic response if declared boolean.
602+
*
603+
* @return the boolean
604+
*/
605+
public boolean isOverrideWithGenericResponseIfDeclared() {
606+
return overrideWithGenericResponseIfDeclared;
607+
}
608+
609+
/**
610+
* Sets override with generic response if declared.
611+
*
612+
* @param overrideWithGenericResponseIfDeclared the override with generic response if declared
613+
*/
614+
public void setOverrideWithGenericResponseIfDeclared(boolean overrideWithGenericResponseIfDeclared) {
615+
this.overrideWithGenericResponseIfDeclared = overrideWithGenericResponseIfDeclared;
616+
}
617+
595618
/**
596619
* Is remove broken reference definitions boolean.
597620
*

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ OpenAPIService openAPIBuilder(Optional<OpenAPI> openAPI,
228228
SecurityService securityParser,
229229
SpringDocConfigProperties springDocConfigProperties,PropertyResolverUtils propertyResolverUtils,
230230
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
231-
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers) {
232-
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers);
231+
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider) {
232+
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
233233
}
234234

235235
/**

springdoc-openapi-common/src/main/java/org/springdoc/core/providers/JavadocProvider.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,22 @@
2222

2323
import java.lang.reflect.Field;
2424
import java.lang.reflect.Method;
25+
import java.util.Map;
2526

2627
/**
2728
* The interface Javadoc provider.
2829
* @author bnasslashen
2930
*/
3031
public interface JavadocProvider {
3132

33+
/**
34+
* Gets class description.
35+
*
36+
* @param cl the class
37+
* @return the class description
38+
*/
39+
String getClassJavadoc(Class<?> cl);
40+
3241
/**
3342
* Gets method description.
3443
*
@@ -45,6 +54,14 @@ public interface JavadocProvider {
4554
*/
4655
String getMethodJavadocReturn(Method method);
4756

57+
/**
58+
* Gets method throws declaration.
59+
*
60+
* @param method the method
61+
* @return the method throws (name-description map)
62+
*/
63+
Map<String, String> getMethodJavadocThrows(Method method);
64+
4865
/**
4966
* Gets param javadoc.
5067
*
@@ -55,5 +72,12 @@ public interface JavadocProvider {
5572
String getParamJavadoc(Method method, String name);
5673

5774
String getFieldJavadoc(Field field);
75+
76+
/**
77+
* Returns the first sentence of a javadoc comment.
78+
* @param text the javadoc comment's text
79+
* @return the first sentence based on javadoc guidelines
80+
*/
81+
String getFirstSentence(String text);
5882
}
5983

springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizer.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato
7676
if (!CollectionUtils.isEmpty(fields)) {
7777
if (!type.isSchemaProperty()) {
7878
Schema existingSchema = context.resolve(type);
79-
setJavadocDescription(fields, existingSchema);
79+
setJavadocDescription(cls, fields, existingSchema);
8080
}
8181
else if (resolvedSchema != null && resolvedSchema.get$ref() != null && resolvedSchema.get$ref().contains(AnnotationsUtils.COMPONENTS_REF)) {
8282
String schemaName = resolvedSchema.get$ref().substring(21);
8383
Schema existingSchema = context.getDefinedModels().get(schemaName);
84-
setJavadocDescription(fields, existingSchema);
84+
setJavadocDescription(cls, fields, existingSchema);
8585
}
8686
}
8787
return resolvedSchema;
@@ -96,8 +96,11 @@ else if (resolvedSchema != null && resolvedSchema.get$ref() != null && resolvedS
9696
* @param fields the fields
9797
* @param existingSchema the existing schema
9898
*/
99-
private void setJavadocDescription(List<Field> fields, Schema existingSchema) {
99+
private void setJavadocDescription(Class<?> cls, List<Field> fields, Schema existingSchema) {
100100
if (existingSchema != null) {
101+
if (StringUtils.isBlank(existingSchema.getDescription())) {
102+
existingSchema.setDescription(javadocProvider.getClassJavadoc(cls));
103+
}
101104
Map<String, Schema> properties = existingSchema.getProperties();
102105
if (!CollectionUtils.isEmpty(properties))
103106
properties.entrySet().stream()

0 commit comments

Comments
 (0)