Skip to content

Commit eb53db7

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 b7380a6 commit eb53db7

31 files changed

+1515
-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;
@@ -62,6 +64,7 @@
6264
import org.springframework.web.method.ControllerAdviceBean;
6365
import org.springframework.web.method.HandlerMethod;
6466

67+
import static java.util.Arrays.asList;
6568
import static org.springdoc.core.Constants.DEFAULT_DESCRIPTION;
6669
import static org.springdoc.core.SpringDocAnnotationsUtils.extractSchema;
6770
import static org.springdoc.core.SpringDocAnnotationsUtils.getContent;
@@ -73,6 +76,11 @@
7376
* @author bnasslahsen
7477
*/
7578
public class GenericResponseService {
79+
/**
80+
* This extension name is used to temporary store
81+
* the exception classes.
82+
*/
83+
private static final String EXTENSION_EXCEPTION_CLASSES = "x-exception-class";
7684

7785
/**
7886
* The Operation builder.
@@ -133,7 +141,11 @@ public GenericResponseService(OperationService operationService, List<ReturnType
133141
*/
134142
public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
135143
MethodAttributes methodAttributes) {
136-
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(getGenericMapResponse(handlerMethod.getBeanType()));
144+
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod.getBeanType());
145+
if (springDocConfigProperties.isOverrideWithGenericResponseIfDeclared()) {
146+
genericMapResponse = filterAndEnrichGenericMapResponseByDeclarations(handlerMethod, genericMapResponse);
147+
}
148+
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(genericMapResponse);
137149
//Then use the apiResponses from documentation
138150
ApiResponses apiResponsesFromDoc = operation.getResponses();
139151
if (!CollectionUtils.isEmpty(apiResponsesFromDoc))
@@ -145,6 +157,41 @@ public ApiResponses build(Components components, HandlerMethod handlerMethod, Op
145157
return apiResponses;
146158
}
147159

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

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

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
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;
6061

6162
import org.springframework.beans.BeansException;
6263
import org.springframework.beans.factory.config.BeanDefinition;
@@ -155,6 +156,11 @@ public class OpenAPIService implements ApplicationContextAware {
155156
*/
156157
private PropertyResolverUtils propertyResolverUtils;
157158

159+
/**
160+
* The javadoc provider.
161+
*/
162+
private Optional<JavadocProvider> javadocProvider;
163+
158164
/**
159165
* The Basic error controller.
160166
*/
@@ -189,7 +195,8 @@ public class OpenAPIService implements ApplicationContextAware {
189195
public OpenAPIService(Optional<OpenAPI> openAPI, SecurityService securityParser,
190196
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
191197
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
192-
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers) {
198+
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
199+
Optional<JavadocProvider> javadocProvider) {
193200
if (openAPI.isPresent()) {
194201
this.openAPI = openAPI.get();
195202
if (this.openAPI.getComponents() == null)
@@ -204,6 +211,7 @@ public OpenAPIService(Optional<OpenAPI> openAPI, SecurityService securityParser,
204211
this.springDocConfigProperties = springDocConfigProperties;
205212
this.openApiBuilderCustomisers = openApiBuilderCustomizers;
206213
this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
214+
this.javadocProvider = javadocProvider;
207215
if (springDocConfigProperties.isUseFqn())
208216
TypeNameResolver.std.setUseFqn(true);
209217
}
@@ -348,8 +356,19 @@ public Operation buildTags(HandlerMethod handlerMethod, Operation operation, Ope
348356
}
349357
}
350358

351-
if (isAutoTagClasses(operation))
352-
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+
}
353372

354373
if (!CollectionUtils.isEmpty(tags)) {
355374
// Existing tags
@@ -733,7 +752,7 @@ public SecurityService getSecurityParser() {
733752

734753
/**
735754
* Gets server base URL
736-
*
755+
*
737756
* @return the server base URL
738757
*/
739758
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)