Skip to content

Commit 57a8fa2

Browse files
committed
Changes report: Updated class and method javadoc handling. Fixes #1579.
1 parent 9528d8f commit 57a8fa2

File tree

171 files changed

+2931
-437
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

171 files changed

+2931
-437
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
317317
Map<String, Object> findControllerAdvice = openAPIService.getControllerAdviceMap();
318318
// calculate generic responses
319319
openApi = openAPIService.getCalculatedOpenAPI();
320-
if (springDocConfigProperties.isOverrideWithGenericResponse()) {
320+
if (springDocConfigProperties.isDefaultOverrideWithGenericResponse()) {
321321
if (!CollectionUtils.isEmpty(mappingsMap))
322322
findControllerAdvice.putAll(mappingsMap);
323323
responseBuilder.buildGenericResponse(openApi.getComponents(), findControllerAdvice, finalLocale);
@@ -465,8 +465,18 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
465465
// get javadoc method description
466466
if (javadocProvider != null) {
467467
String description = javadocProvider.getMethodJavadocDescription(handlerMethod.getMethod());
468-
if (!StringUtils.isEmpty(description) && StringUtils.isEmpty(operation.getDescription()))
468+
String summary = javadocProvider.getFirstSentence(description);
469+
boolean emptyOverrideDescription = StringUtils.isEmpty(operation.getDescription());
470+
boolean emptyOverrideSummary = StringUtils.isEmpty(operation.getSummary());
471+
if (!StringUtils.isEmpty(description) && emptyOverrideDescription) {
469472
operation.setDescription(description);
473+
}
474+
// if there is a previously set description
475+
// but no summary then it is intentional
476+
// we keep it as is
477+
if (!StringUtils.isEmpty(summary) && emptyOverrideSummary && emptyOverrideDescription) {
478+
operation.setSummary(javadocProvider.getFirstSentence(description));
479+
}
470480
}
471481

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

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ PolymorphicModelConverter polymorphicModelConverter() {
227227
* @param springDocConfigProperties the spring doc config properties
228228
* @param propertyResolverUtils the property resolver utils
229229
* @param openApiBuilderCustomizers the open api builder customisers
230+
* @param serverBaseUrlCustomizers the server base url customizers
231+
* @param javadocProvider the javadoc provider
230232
* @return the open api builder
231233
*/
232234
@Bean
@@ -236,8 +238,9 @@ OpenAPIService openAPIBuilder(Optional<OpenAPI> openAPI,
236238
SecurityService securityParser,
237239
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
238240
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
239-
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers) {
240-
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomisers);
241+
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
242+
Optional<JavadocProvider> javadocProvider) {
243+
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
241244
}
242245

243246
/**

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/JavadocPropertyCustomizer.java

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

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public class SpringDocConfigProperties {
119119
/**
120120
* The Override with generic response.
121121
*/
122-
private boolean overrideWithGenericResponse = true;
122+
private Boolean overrideWithGenericResponse;
123123

124124
/**
125125
* The Remove broken reference definitions.
@@ -745,10 +745,21 @@ public void setDefaultProducesMediaType(String defaultProducesMediaType) {
745745
*
746746
* @return the boolean
747747
*/
748-
public boolean isOverrideWithGenericResponse() {
749-
return overrideWithGenericResponse;
748+
public Boolean isOverrideWithGenericResponse() {
749+
return overrideWithGenericResponse != null && overrideWithGenericResponse;
750750
}
751751

752+
/**
753+
* Gets default override with generic response.
754+
*
755+
* @return the default override with generic response
756+
*/
757+
public boolean isDefaultOverrideWithGenericResponse() {
758+
if (overrideWithGenericResponse == null)
759+
return true;
760+
else
761+
return overrideWithGenericResponse;
762+
}
752763
/**
753764
* Sets override with generic response.
754765
*

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

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

2525
import java.lang.reflect.Field;
2626
import java.lang.reflect.Method;
27+
import java.util.Map;
2728

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

35+
/**
36+
* Gets class description.
37+
*
38+
* @param cl the class
39+
* @return the class description
40+
*/
41+
String getClassJavadoc(Class<?> cl);
42+
3443
/**
3544
* Gets method description.
3645
*
@@ -47,6 +56,14 @@ public interface JavadocProvider {
4756
*/
4857
String getMethodJavadocReturn(Method method);
4958

59+
/**
60+
* Gets method throws declaration.
61+
*
62+
* @param method the method
63+
* @return the method throws (name-description map)
64+
*/
65+
Map<String, String> getMethodJavadocThrows(Method method);
66+
5067
/**
5168
* Gets param javadoc.
5269
*
@@ -56,6 +73,20 @@ public interface JavadocProvider {
5673
*/
5774
String getParamJavadoc(Method method, String name);
5875

76+
/**
77+
* Gets field javadoc.
78+
*
79+
* @param field the field
80+
* @return the field javadoc
81+
*/
5982
String getFieldJavadoc(Field field);
83+
84+
85+
/**
86+
* Returns the first sentence of a javadoc comment.
87+
* @param text the javadoc comment's text
88+
* @return the first sentence based on javadoc guidelines
89+
*/
90+
String getFirstSentence(String text);
6091
}
6192

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,19 @@
2525
import java.lang.reflect.Field;
2626
import java.lang.reflect.Method;
2727
import java.util.List;
28+
import java.util.Map;
2829

30+
import com.github.therapi.runtimejavadoc.ClassJavadoc;
2931
import com.github.therapi.runtimejavadoc.CommentFormatter;
3032
import com.github.therapi.runtimejavadoc.FieldJavadoc;
3133
import com.github.therapi.runtimejavadoc.MethodJavadoc;
3234
import com.github.therapi.runtimejavadoc.ParamJavadoc;
3335
import com.github.therapi.runtimejavadoc.RuntimeJavadoc;
36+
import com.github.therapi.runtimejavadoc.ThrowsJavadoc;
37+
import org.apache.commons.lang3.StringUtils;
38+
39+
import static java.lang.Math.min;
40+
import static java.util.stream.Collectors.toMap;
3441

3542
/**
3643
* The type Spring doc javadoc provider.
@@ -43,6 +50,19 @@ public class SpringDocJavadocProvider implements JavadocProvider {
4350
*/
4451
private final CommentFormatter formatter = new CommentFormatter();
4552

53+
54+
/**
55+
* Gets class description.
56+
*
57+
* @param cl the class
58+
* @return the class description
59+
*/
60+
@Override
61+
public String getClassJavadoc(Class<?> cl) {
62+
ClassJavadoc classJavadoc = RuntimeJavadoc.getJavadoc(cl);
63+
return formatter.format(classJavadoc.getComment());
64+
}
65+
4666
/**
4767
* Gets method javadoc description.
4868
*
@@ -67,6 +87,19 @@ public String getMethodJavadocReturn(Method method) {
6787
return formatter.format(methodJavadoc.getReturns());
6888
}
6989

90+
/**
91+
* Gets method throws declaration.
92+
*
93+
* @param method the method
94+
* @return the method throws (name-description map)
95+
*/
96+
public Map<String, String> getMethodJavadocThrows(Method method) {
97+
return RuntimeJavadoc.getJavadoc(method)
98+
.getThrows()
99+
.stream()
100+
.collect(toMap(ThrowsJavadoc::getName, javadoc -> formatter.format(javadoc.getComment())));
101+
}
102+
70103
/**
71104
* Gets param javadoc.
72105
*
@@ -94,4 +127,31 @@ public String getFieldJavadoc(Field field) {
94127
return formatter.format(fieldJavadoc.getComment());
95128
}
96129

130+
@Override
131+
public String getFirstSentence(String text) {
132+
if (StringUtils.isEmpty(text)) {
133+
return text;
134+
}
135+
int pOpenIndex = text.indexOf("<p>");
136+
int pCloseIndex = text.indexOf("</p>");
137+
int dotIndex = text.indexOf(".");
138+
if (pOpenIndex != -1) {
139+
if (pOpenIndex == 0 && pCloseIndex != -1) {
140+
if (dotIndex != -1) {
141+
return text.substring(3, min(pCloseIndex, dotIndex));
142+
}
143+
return text.substring(3, pCloseIndex);
144+
}
145+
if (dotIndex != -1) {
146+
return text.substring(0, min(pOpenIndex, dotIndex));
147+
}
148+
return text.substring(0, pOpenIndex);
149+
}
150+
if (dotIndex != -1
151+
&& text.length() != dotIndex + 1
152+
&& Character.isWhitespace(text.charAt(dotIndex + 1))) {
153+
return text.substring(0, dotIndex + 1);
154+
}
155+
return text;
156+
}
97157
}

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424

2525
import java.lang.annotation.Annotation;
2626
import java.lang.reflect.Method;
27+
import java.lang.reflect.Parameter;
2728
import java.lang.reflect.ParameterizedType;
2829
import java.lang.reflect.Type;
2930
import java.util.ArrayList;
3031
import java.util.Arrays;
32+
import java.util.HashMap;
3133
import java.util.HashSet;
3234
import java.util.LinkedHashMap;
3335
import java.util.List;
@@ -70,6 +72,7 @@
7072
import org.springframework.web.method.ControllerAdviceBean;
7173
import org.springframework.web.method.HandlerMethod;
7274

75+
import static java.util.Arrays.asList;
7376
import static org.springdoc.core.converters.ConverterUtils.isResponseTypeWrapper;
7477
import static org.springdoc.core.utils.Constants.DEFAULT_DESCRIPTION;
7578
import static org.springdoc.core.utils.SpringDocAnnotationsUtils.extractSchema;
@@ -82,6 +85,12 @@
8285
*/
8386
public class GenericResponseService {
8487

88+
/**
89+
* This extension name is used to temporary store
90+
* the exception classes.
91+
*/
92+
private static final String EXTENSION_EXCEPTION_CLASSES = "x-exception-class";
93+
8594
/**
8695
* The Operation builder.
8796
*/
@@ -141,7 +150,11 @@ public GenericResponseService(OperationService operationService, List<ReturnType
141150
*/
142151
public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
143152
MethodAttributes methodAttributes) {
144-
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(getGenericMapResponse(handlerMethod.getBeanType()));
153+
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod.getBeanType());
154+
if (springDocConfigProperties.isOverrideWithGenericResponse()) {
155+
genericMapResponse = filterAndEnrichGenericMapResponseByDeclarations(handlerMethod, genericMapResponse);
156+
}
157+
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(genericMapResponse);
145158
//Then use the apiResponses from documentation
146159
ApiResponses apiResponsesFromDoc = operation.getResponses();
147160
if (!CollectionUtils.isEmpty(apiResponsesFromDoc))
@@ -153,6 +166,41 @@ public ApiResponses build(Components components, HandlerMethod handlerMethod, Op
153166
return apiResponses;
154167
}
155168

169+
/**
170+
* Filters the generic API responses by the declared exceptions.
171+
* If Javadoc comment found for the declaration than it overrides the default description.
172+
*
173+
* @param handlerMethod the method which can have exception declarations
174+
* @param genericMapResponse the default generic API responses
175+
* @return the filtered and enriched responses
176+
*/
177+
private Map<String, ApiResponse> filterAndEnrichGenericMapResponseByDeclarations(HandlerMethod handlerMethod, Map<String, ApiResponse> genericMapResponse) {
178+
Map<String, ApiResponse> result = new HashMap<>();
179+
for (Map.Entry<String, ApiResponse> genericResponse : genericMapResponse.entrySet()) {
180+
Map<String, Object> extensions = genericResponse.getValue().getExtensions();
181+
Set<Class<?>> genericExceptions = (Set<Class<?>>) extensions.get(EXTENSION_EXCEPTION_CLASSES);
182+
for (Class<?> declaredException : handlerMethod.getMethod().getExceptionTypes()) {
183+
if (genericExceptions.contains(declaredException)) {
184+
ApiResponse clone = cloneApiResponse(genericResponse.getValue());
185+
clone.getExtensions().remove(EXTENSION_EXCEPTION_CLASSES);
186+
if (operationService.getJavadocProvider() != null) {
187+
JavadocProvider javadocProvider = operationService.getJavadocProvider();
188+
Map<String, String> javadocThrows = javadocProvider.getMethodJavadocThrows(handlerMethod.getMethod());
189+
String description = javadocThrows.get(declaredException.getName());
190+
if (description == null) {
191+
description = javadocThrows.get(declaredException.getSimpleName());
192+
}
193+
if (description != null && !description.trim().isEmpty()) {
194+
clone.setDescription(description);
195+
}
196+
}
197+
result.put(genericResponse.getKey(), clone);
198+
}
199+
}
200+
}
201+
return result;
202+
}
203+
156204
/**
157205
* Build generic response.
158206
*
@@ -510,6 +558,23 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent()))
510558
if (schemaN != null && ArrayUtils.isNotEmpty(methodAttributes.getMethodProduces()))
511559
Arrays.stream(methodAttributes.getMethodProduces()).forEach(mediaTypeStr -> mergeSchema(existingContent, schemaN, mediaTypeStr));
512560
}
561+
if (springDocConfigProperties.isOverrideWithGenericResponse()
562+
&& methodParameter.getExecutable().isAnnotationPresent(ExceptionHandler.class)) {
563+
// ExceptionHandler's exception class resolution is non-trivial
564+
// more info on its javadoc
565+
ExceptionHandler exceptionHandler = methodParameter.getExecutable().getAnnotation(ExceptionHandler.class);
566+
Set<Class<?>> exceptions = new HashSet<>();
567+
if (exceptionHandler.value().length == 0) {
568+
for (Parameter parameter : methodParameter.getExecutable().getParameters()) {
569+
if (Throwable.class.isAssignableFrom(parameter.getType())) {
570+
exceptions.add(parameter.getType());
571+
}
572+
}
573+
} else {
574+
exceptions.addAll(asList(exceptionHandler.value()));
575+
}
576+
apiResponse.addExtension(EXTENSION_EXCEPTION_CLASSES, exceptions);
577+
}
513578
apiResponsesOp.addApiResponse(httpCode, apiResponse);
514579
}
515580

@@ -628,4 +693,16 @@ private boolean isHttpCodePresent(String httpCode, Set<io.swagger.v3.oas.annotat
628693
public static void setResponseEntityExceptionHandlerClass(Class<?> responseEntityExceptionHandlerClass) {
629694
GenericResponseService.responseEntityExceptionHandlerClass = responseEntityExceptionHandlerClass;
630695
}
696+
697+
698+
private ApiResponse cloneApiResponse(ApiResponse original) {
699+
ApiResponse clone = new ApiResponse();
700+
clone.set$ref(original.get$ref());
701+
clone.setDescription(original.getDescription());
702+
clone.setContent(original.getContent());
703+
clone.setHeaders(original.getHeaders() == null ? null : new HashMap<>(original.getHeaders()));
704+
clone.setExtensions(original.getExtensions() == null ? null : new HashMap<>(original.getExtensions()));
705+
clone.setLinks(original.getLinks() == null ? null : new HashMap<>(original.getLinks()));
706+
return clone;
707+
}
631708
}

0 commit comments

Comments
 (0)