Skip to content

Commit

Permalink
Changes report: Updated class and method javadoc handling. Fixes #1579.
Browse files Browse the repository at this point in the history
  • Loading branch information
bnasslahsen committed Apr 20, 2022
1 parent 9528d8f commit 57a8fa2
Show file tree
Hide file tree
Showing 171 changed files with 2,931 additions and 437 deletions.
Expand Up @@ -317,7 +317,7 @@ protected synchronized OpenAPI getOpenApi(Locale locale) {
Map<String, Object> findControllerAdvice = openAPIService.getControllerAdviceMap();
// calculate generic responses
openApi = openAPIService.getCalculatedOpenAPI();
if (springDocConfigProperties.isOverrideWithGenericResponse()) {
if (springDocConfigProperties.isDefaultOverrideWithGenericResponse()) {
if (!CollectionUtils.isEmpty(mappingsMap))
findControllerAdvice.putAll(mappingsMap);
responseBuilder.buildGenericResponse(openApi.getComponents(), findControllerAdvice, finalLocale);
Expand Down Expand Up @@ -465,8 +465,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
Expand Up @@ -227,6 +227,8 @@ PolymorphicModelConverter polymorphicModelConverter() {
* @param springDocConfigProperties the spring doc config properties
* @param propertyResolverUtils the property resolver utils
* @param openApiBuilderCustomizers the open api builder customisers
* @param serverBaseUrlCustomizers the server base url customizers
* @param javadocProvider the javadoc provider
* @return the open api builder
*/
@Bean
Expand All @@ -236,8 +238,9 @@ OpenAPIService openAPIBuilder(Optional<OpenAPI> openAPI,
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers) {
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomisers);
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
}

/**
Expand Down
Expand Up @@ -75,12 +75,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 @@ -95,8 +95,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
Expand Up @@ -119,7 +119,7 @@ public class SpringDocConfigProperties {
/**
* The Override with generic response.
*/
private boolean overrideWithGenericResponse = true;
private Boolean overrideWithGenericResponse;

/**
* The Remove broken reference definitions.
Expand Down Expand Up @@ -745,10 +745,21 @@ public void setDefaultProducesMediaType(String defaultProducesMediaType) {
*
* @return the boolean
*/
public boolean isOverrideWithGenericResponse() {
return overrideWithGenericResponse;
public Boolean isOverrideWithGenericResponse() {
return overrideWithGenericResponse != null && overrideWithGenericResponse;
}

/**
* Gets default override with generic response.
*
* @return the default override with generic response
*/
public boolean isDefaultOverrideWithGenericResponse() {
if (overrideWithGenericResponse == null)
return true;
else
return overrideWithGenericResponse;
}
/**
* Sets override with generic response.
*
Expand Down
Expand Up @@ -24,13 +24,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 @@ -47,6 +56,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 @@ -56,6 +73,20 @@ public interface JavadocProvider {
*/
String getParamJavadoc(Method method, String name);

/**
* Gets field javadoc.
*
* @param field the field
* @return the field javadoc
*/
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);
}

Expand Up @@ -25,12 +25,19 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

import com.github.therapi.runtimejavadoc.ClassJavadoc;
import com.github.therapi.runtimejavadoc.CommentFormatter;
import com.github.therapi.runtimejavadoc.FieldJavadoc;
import com.github.therapi.runtimejavadoc.MethodJavadoc;
import com.github.therapi.runtimejavadoc.ParamJavadoc;
import com.github.therapi.runtimejavadoc.RuntimeJavadoc;
import com.github.therapi.runtimejavadoc.ThrowsJavadoc;
import org.apache.commons.lang3.StringUtils;

import static java.lang.Math.min;
import static java.util.stream.Collectors.toMap;

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


/**
* Gets class description.
*
* @param cl the class
* @return the class description
*/
@Override
public String getClassJavadoc(Class<?> cl) {
ClassJavadoc classJavadoc = RuntimeJavadoc.getJavadoc(cl);
return formatter.format(classJavadoc.getComment());
}

/**
* Gets method javadoc description.
*
Expand All @@ -67,6 +87,19 @@ public String getMethodJavadocReturn(Method method) {
return formatter.format(methodJavadoc.getReturns());
}

/**
* Gets method throws declaration.
*
* @param method the method
* @return the method throws (name-description map)
*/
public Map<String, String> getMethodJavadocThrows(Method method) {
return RuntimeJavadoc.getJavadoc(method)
.getThrows()
.stream()
.collect(toMap(ThrowsJavadoc::getName, javadoc -> formatter.format(javadoc.getComment())));
}

/**
* Gets param javadoc.
*
Expand Down Expand Up @@ -94,4 +127,31 @@ public String getFieldJavadoc(Field field) {
return formatter.format(fieldJavadoc.getComment());
}

@Override
public String getFirstSentence(String text) {
if (StringUtils.isEmpty(text)) {
return text;
}
int pOpenIndex = text.indexOf("<p>");
int pCloseIndex = text.indexOf("</p>");
int dotIndex = text.indexOf(".");
if (pOpenIndex != -1) {
if (pOpenIndex == 0 && pCloseIndex != -1) {
if (dotIndex != -1) {
return text.substring(3, min(pCloseIndex, dotIndex));
}
return text.substring(3, pCloseIndex);
}
if (dotIndex != -1) {
return text.substring(0, min(pOpenIndex, dotIndex));
}
return text.substring(0, pOpenIndex);
}
if (dotIndex != -1
&& text.length() != dotIndex + 1
&& Character.isWhitespace(text.charAt(dotIndex + 1))) {
return text.substring(0, dotIndex + 1);
}
return text;
}
}
Expand Up @@ -24,10 +24,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 @@ -70,6 +72,7 @@
import org.springframework.web.method.ControllerAdviceBean;
import org.springframework.web.method.HandlerMethod;

import static java.util.Arrays.asList;
import static org.springdoc.core.converters.ConverterUtils.isResponseTypeWrapper;
import static org.springdoc.core.utils.Constants.DEFAULT_DESCRIPTION;
import static org.springdoc.core.utils.SpringDocAnnotationsUtils.extractSchema;
Expand All @@ -82,6 +85,12 @@
*/
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 @@ -141,7 +150,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.isOverrideWithGenericResponse()) {
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 @@ -153,6 +166,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 @@ -510,6 +558,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.isOverrideWithGenericResponse()
&& 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 @@ -628,4 +693,16 @@ 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;
}
}

0 comments on commit 57a8fa2

Please sign in to comment.