Skip to content

Updated class and method javadoc handling #1579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
Map<String, Object> findControllerAdvice = openAPIService.getControllerAdviceMap();
// calculate generic responses
openApi = openAPIService.getCalculatedOpenAPI();
if (springDocConfigProperties.isOverrideWithGenericResponse()) {
if (springDocConfigProperties.isOverrideWithGenericResponse() || springDocConfigProperties.isOverrideWithGenericResponseIfDeclared()) {
if (!CollectionUtils.isEmpty(mappingsMap))
findControllerAdvice.putAll(mappingsMap);
responseBuilder.buildGenericResponse(openApi.getComponents(), findControllerAdvice, finalLocale);
Expand Down Expand Up @@ -463,8 +463,18 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
// get javadoc method description
if (javadocProvider != null) {
String description = javadocProvider.getMethodJavadocDescription(handlerMethod.getMethod());
if (!StringUtils.isEmpty(description) && StringUtils.isEmpty(operation.getDescription()))
String summary = javadocProvider.getFirstSentence(description);
boolean emptyOverrideDescription = StringUtils.isEmpty(operation.getDescription());
boolean emptyOverrideSummary = StringUtils.isEmpty(operation.getSummary());
if (!StringUtils.isEmpty(description) && emptyOverrideDescription) {
operation.setDescription(description);
}
// if there is a previously set description
// but no summary then it is intentional
// we keep it as is
if (!StringUtils.isEmpty(summary) && emptyOverrideSummary && emptyOverrideDescription) {
operation.setSummary(javadocProvider.getFirstSentence(description));
}
}

Set<io.swagger.v3.oas.annotations.callbacks.Callback> apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.callbacks.Callback.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -62,6 +64,7 @@
import org.springframework.web.method.ControllerAdviceBean;
import org.springframework.web.method.HandlerMethod;

import static java.util.Arrays.asList;
import static org.springdoc.core.Constants.DEFAULT_DESCRIPTION;
import static org.springdoc.core.SpringDocAnnotationsUtils.extractSchema;
import static org.springdoc.core.SpringDocAnnotationsUtils.getContent;
Expand All @@ -73,6 +76,11 @@
* @author bnasslahsen
*/
public class GenericResponseService {
/**
* This extension name is used to temporary store
* the exception classes.
*/
private static final String EXTENSION_EXCEPTION_CLASSES = "x-exception-class";

/**
* The Operation builder.
Expand Down Expand Up @@ -133,7 +141,11 @@ public GenericResponseService(OperationService operationService, List<ReturnType
*/
public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
MethodAttributes methodAttributes) {
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(getGenericMapResponse(handlerMethod.getBeanType()));
Map<String, ApiResponse> genericMapResponse = getGenericMapResponse(handlerMethod.getBeanType());
if (springDocConfigProperties.isOverrideWithGenericResponseIfDeclared()) {
genericMapResponse = filterAndEnrichGenericMapResponseByDeclarations(handlerMethod, genericMapResponse);
}
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(genericMapResponse);
//Then use the apiResponses from documentation
ApiResponses apiResponsesFromDoc = operation.getResponses();
if (!CollectionUtils.isEmpty(apiResponsesFromDoc))
Expand All @@ -145,6 +157,41 @@ public ApiResponses build(Components components, HandlerMethod handlerMethod, Op
return apiResponses;
}

/**
* Filters the generic API responses by the declared exceptions.
* If Javadoc comment found for the declaration than it overrides the default description.
*
* @param handlerMethod the method which can have exception declarations
* @param genericMapResponse the default generic API responses
* @return the filtered and enriched responses
*/
private Map<String, ApiResponse> filterAndEnrichGenericMapResponseByDeclarations(HandlerMethod handlerMethod, Map<String, ApiResponse> genericMapResponse) {
Map<String, ApiResponse> result = new HashMap<>();
for (Map.Entry<String, ApiResponse> genericResponse : genericMapResponse.entrySet()) {
Map<String, Object> extensions = genericResponse.getValue().getExtensions();
Set<Class<?>> genericExceptions = (Set<Class<?>>) extensions.get(EXTENSION_EXCEPTION_CLASSES);
for (Class<?> declaredException : handlerMethod.getMethod().getExceptionTypes()) {
if (genericExceptions.contains(declaredException)) {
ApiResponse clone = cloneApiResponse(genericResponse.getValue());
clone.getExtensions().remove(EXTENSION_EXCEPTION_CLASSES);
if (operationService.getJavadocProvider() != null) {
JavadocProvider javadocProvider = operationService.getJavadocProvider();
Map<String, String> javadocThrows = javadocProvider.getMethodJavadocThrows(handlerMethod.getMethod());
String description = javadocThrows.get(declaredException.getName());
if (description == null) {
description = javadocThrows.get(declaredException.getSimpleName());
}
if (description != null && !description.trim().isEmpty()) {
clone.setDescription(description);
}
}
result.put(genericResponse.getKey(), clone);
}
}
}
return result;
}

/**
* Build generic response.
*
Expand Down Expand Up @@ -502,6 +549,23 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent()))
if (schemaN != null && ArrayUtils.isNotEmpty(methodAttributes.getMethodProduces()))
Arrays.stream(methodAttributes.getMethodProduces()).forEach(mediaTypeStr -> mergeSchema(existingContent, schemaN, mediaTypeStr));
}
if (springDocConfigProperties.isOverrideWithGenericResponseIfDeclared()
&& methodParameter.getExecutable().isAnnotationPresent(ExceptionHandler.class)) {
// ExceptionHandler's exception class resolution is non-trivial
// more info on its javadoc
ExceptionHandler exceptionHandler = methodParameter.getExecutable().getAnnotation(ExceptionHandler.class);
Set<Class<?>> exceptions = new HashSet<>();
if (exceptionHandler.value().length == 0) {
for (Parameter parameter : methodParameter.getExecutable().getParameters()) {
if (Throwable.class.isAssignableFrom(parameter.getType())) {
exceptions.add(parameter.getType());
}
}
} else {
exceptions.addAll(asList(exceptionHandler.value()));
}
apiResponse.addExtension(EXTENSION_EXCEPTION_CLASSES, exceptions);
}
apiResponsesOp.addApiResponse(httpCode, apiResponse);
}

Expand Down Expand Up @@ -620,4 +684,15 @@ private boolean isHttpCodePresent(String httpCode, Set<io.swagger.v3.oas.annotat
public static void setResponseEntityExceptionHandlerClass(Class<?> responseEntityExceptionHandlerClass) {
GenericResponseService.responseEntityExceptionHandlerClass = responseEntityExceptionHandlerClass;
}

private ApiResponse cloneApiResponse(ApiResponse original) {
ApiResponse clone = new ApiResponse();
clone.set$ref(original.get$ref());
clone.setDescription(original.getDescription());
clone.setContent(original.getContent());
clone.setHeaders(original.getHeaders() == null ? null : new HashMap<>(original.getHeaders()));
clone.setExtensions(original.getExtensions() == null ? null : new HashMap<>(original.getExtensions()));
clone.setLinks(original.getLinks() == null ? null : new HashMap<>(original.getLinks()));
return clone;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.slf4j.LoggerFactory;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.providers.JavadocProvider;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
Expand Down Expand Up @@ -155,6 +156,11 @@ public class OpenAPIService implements ApplicationContextAware {
*/
private PropertyResolverUtils propertyResolverUtils;

/**
* The javadoc provider.
*/
private Optional<JavadocProvider> javadocProvider;

/**
* The Basic error controller.
*/
Expand Down Expand Up @@ -189,7 +195,8 @@ public class OpenAPIService implements ApplicationContextAware {
public OpenAPIService(Optional<OpenAPI> openAPI, SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers) {
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
if (openAPI.isPresent()) {
this.openAPI = openAPI.get();
if (this.openAPI.getComponents() == null)
Expand All @@ -204,6 +211,7 @@ public OpenAPIService(Optional<OpenAPI> openAPI, SecurityService securityParser,
this.springDocConfigProperties = springDocConfigProperties;
this.openApiBuilderCustomisers = openApiBuilderCustomizers;
this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
this.javadocProvider = javadocProvider;
if (springDocConfigProperties.isUseFqn())
TypeNameResolver.std.setUseFqn(true);
}
Expand Down Expand Up @@ -348,8 +356,21 @@ public Operation buildTags(HandlerMethod handlerMethod, Operation operation, Ope
}
}

if (isAutoTagClasses(operation))
operation.addTagsItem(splitCamelCase(handlerMethod.getBeanType().getSimpleName()));
if (isAutoTagClasses(operation)) {
String tagAutoName = splitCamelCase(handlerMethod.getBeanType().getSimpleName());
operation.addTagsItem(tagAutoName);
if (javadocProvider.isPresent()) {
String description = javadocProvider.get().getClassJavadoc(handlerMethod.getBeanType());
if (StringUtils.isNotBlank(description)) {
io.swagger.v3.oas.models.tags.Tag tag = new io.swagger.v3.oas.models.tags.Tag();
tag.setName(tagAutoName);
tag.setDescription(description);
if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
openAPI.addTagsItem(tag);
}
}
}
}

if (!CollectionUtils.isEmpty(tags)) {
// Existing tags
Expand Down Expand Up @@ -733,7 +754,7 @@ public SecurityService getSecurityParser() {

/**
* Gets server base URL
*
*
* @return the server base URL
*/
public String getServerBaseUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ public class SpringDocConfigProperties {
*/
private boolean overrideWithGenericResponse = true;

/**
* The Override with generic response only if the exception is declared
*/
private boolean overrideWithGenericResponseIfDeclared = false;

/**
* The Remove broken reference definitions.
*/
Expand Down Expand Up @@ -592,6 +597,24 @@ public void setOverrideWithGenericResponse(boolean overrideWithGenericResponse)
this.overrideWithGenericResponse = overrideWithGenericResponse;
}

/**
* Is override with generic response if declared boolean.
*
* @return the boolean
*/
public boolean isOverrideWithGenericResponseIfDeclared() {
return overrideWithGenericResponseIfDeclared;
}

/**
* Sets override with generic response if declared.
*
* @param overrideWithGenericResponseIfDeclared the override with generic response if declared
*/
public void setOverrideWithGenericResponseIfDeclared(boolean overrideWithGenericResponseIfDeclared) {
this.overrideWithGenericResponseIfDeclared = overrideWithGenericResponseIfDeclared;
}

/**
* Is remove broken reference definitions boolean.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ OpenAPIService openAPIBuilder(Optional<OpenAPI> openAPI,
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties,PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers) {
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers);
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider) {
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,22 @@

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

/**
* The interface Javadoc provider.
* @author bnasslashen
*/
public interface JavadocProvider {

/**
* Gets class description.
*
* @param cl the class
* @return the class description
*/
String getClassJavadoc(Class<?> cl);

/**
* Gets method description.
*
Expand All @@ -45,6 +54,14 @@ public interface JavadocProvider {
*/
String getMethodJavadocReturn(Method method);

/**
* Gets method throws declaration.
*
* @param method the method
* @return the method throws (name-description map)
*/
Map<String, String> getMethodJavadocThrows(Method method);

/**
* Gets param javadoc.
*
Expand All @@ -55,5 +72,12 @@ public interface JavadocProvider {
String getParamJavadoc(Method method, String name);

String getFieldJavadoc(Field field);

/**
* Returns the first sentence of a javadoc comment.
* @param text the javadoc comment's text
* @return the first sentence based on javadoc guidelines
*/
String getFirstSentence(String text);
}

Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato
if (!CollectionUtils.isEmpty(fields)) {
if (!type.isSchemaProperty()) {
Schema existingSchema = context.resolve(type);
setJavadocDescription(fields, existingSchema);
setJavadocDescription(cls, fields, existingSchema);
}
else if (resolvedSchema != null && resolvedSchema.get$ref() != null && resolvedSchema.get$ref().contains(AnnotationsUtils.COMPONENTS_REF)) {
String schemaName = resolvedSchema.get$ref().substring(21);
Schema existingSchema = context.getDefinedModels().get(schemaName);
setJavadocDescription(fields, existingSchema);
setJavadocDescription(cls, fields, existingSchema);
}
}
return resolvedSchema;
Expand All @@ -96,8 +96,11 @@ else if (resolvedSchema != null && resolvedSchema.get$ref() != null && resolvedS
* @param fields the fields
* @param existingSchema the existing schema
*/
private void setJavadocDescription(List<Field> fields, Schema existingSchema) {
private void setJavadocDescription(Class<?> cls, List<Field> fields, Schema existingSchema) {
if (existingSchema != null) {
if (StringUtils.isBlank(existingSchema.getDescription())) {
existingSchema.setDescription(javadocProvider.getClassJavadoc(cls));
}
Map<String, Schema> properties = existingSchema.getProperties();
if (!CollectionUtils.isEmpty(properties))
properties.entrySet().stream()
Expand Down
Loading