From eda5e703c08322304ecb7cc24aa4e04f83b35651 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 15 Mar 2023 00:18:18 +0900 Subject: [PATCH] Fixed a bug that javadoc of record class parameters was not recognized. Fixes #2131 --- .../core/GenericParameterService.java | 34 ++- .../core/providers/JavadocProvider.java | 17 +- .../core/GenericParameterServiceTest.java | 194 ++++++++++++++++++ .../javadoc/JavadocPropertyCustomizer.java | 17 +- .../javadoc/SpringDocJavadocProvider.java | 16 +- .../JavadocPropertyCustomizerTest.java | 134 ++++++++++++ 6 files changed, 392 insertions(+), 20 deletions(-) create mode 100644 springdoc-openapi-common/src/test/java/org/springdoc/core/GenericParameterServiceTest.java create mode 100644 springdoc-openapi-javadoc/src/test/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizerTest.java diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericParameterService.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericParameterService.java index c4192f5c7..3e19e98c3 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericParameterService.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericParameterService.java @@ -2,7 +2,7 @@ * * * * * * - * * * * Copyright 2019-2022 the original author or authors. + * * * * Copyright 2019-2023 the original author or authors. * * * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * * * you may not use this file except in compliance with the License. @@ -720,17 +720,29 @@ public boolean isRequestBodyPresent(ParameterInfo parameterInfo) { String getParamJavadoc(JavadocProvider javadocProvider, MethodParameter methodParameter) { String pName = methodParameter.getParameterName(); DelegatingMethodParameter delegatingMethodParameter = (DelegatingMethodParameter) methodParameter; - final String paramJavadocDescription; - if (delegatingMethodParameter.isParameterObject()) { - String fieldName; - if (StringUtils.isNotEmpty(pName) && pName.contains(DOT)) - fieldName = StringUtils.substringAfterLast(pName, DOT); - else fieldName = pName; - Field field = FieldUtils.getDeclaredField(((DelegatingMethodParameter) methodParameter).getExecutable().getDeclaringClass(), fieldName, true); - paramJavadocDescription = javadocProvider.getFieldJavadoc(field); + if (!delegatingMethodParameter.isParameterObject()) { + return javadocProvider.getParamJavadoc(methodParameter.getMethod(), pName); } - else - paramJavadocDescription = javadocProvider.getParamJavadoc(methodParameter.getMethod(), pName); + String fieldName; + if (StringUtils.isNotEmpty(pName) && pName.contains(DOT)) + fieldName = StringUtils.substringAfterLast(pName, DOT); + else fieldName = pName; + + String paramJavadocDescription = null; + Class cls = ((DelegatingMethodParameter) methodParameter).getExecutable().getDeclaringClass(); + if (cls.getSuperclass() != null && "java.lang.Record".equals(cls.getSuperclass().getName())) { + Map recordParamMap = javadocProvider.getRecordClassParamJavadoc(cls); + if (recordParamMap.containsKey(fieldName)) { + paramJavadocDescription = recordParamMap.get(fieldName); + } + } + + Field field = FieldUtils.getDeclaredField(cls, fieldName, true); + String fieldJavadoc = javadocProvider.getFieldJavadoc(field); + if (StringUtils.isNotBlank(fieldJavadoc)) { + paramJavadocDescription = fieldJavadoc; + } + return paramJavadocDescription; } } diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/providers/JavadocProvider.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/providers/JavadocProvider.java index ff5102143..86ce39b05 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/core/providers/JavadocProvider.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/providers/JavadocProvider.java @@ -2,7 +2,7 @@ * * * * * * - * * * * Copyright 2019-2022 the original author or authors. + * * * * Copyright 2019-2023 the original author or authors. * * * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * * * you may not use this file except in compliance with the License. @@ -41,11 +41,19 @@ public interface JavadocProvider { String getClassJavadoc(Class cl); /** - * Gets method description. + * Gets param descripton of record class. * - * @param method the method - * @return the method description + * @param cl the class + * @return map of field and param descriptions */ + Map getRecordClassParamJavadoc(Class cl); + + /** + * Gets method description. + * + * @param method the method + * @return the method description + */ String getMethodJavadocDescription(Method method); /** @@ -88,4 +96,3 @@ public interface JavadocProvider { */ String getFirstSentence(String text); } - diff --git a/springdoc-openapi-common/src/test/java/org/springdoc/core/GenericParameterServiceTest.java b/springdoc-openapi-common/src/test/java/org/springdoc/core/GenericParameterServiceTest.java new file mode 100644 index 000000000..e43210445 --- /dev/null +++ b/springdoc-openapi-common/src/test/java/org/springdoc/core/GenericParameterServiceTest.java @@ -0,0 +1,194 @@ +/* + * + * * + * * * + * * * * Copyright 2019-2023 the original author or authors. + * * * * + * * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * * you may not use this file except in compliance with the License. + * * * * You may obtain a copy of the License at + * * * * + * * * * https://www.apache.org/licenses/LICENSE-2.0 + * * * * + * * * * Unless required by applicable law or agreed to in writing, software + * * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * * See the License for the specific language governing permissions and + * * * * limitations under the License. + * * * + * * + * + */ + +package org.springdoc.core; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springdoc.core.customizers.DelegatingMethodParameterCustomizer; +import org.springdoc.core.providers.JavadocProvider; +import org.springdoc.core.providers.ObjectMapperProvider; +import org.springdoc.core.providers.WebConversionServiceProvider; + +import org.springframework.core.MethodParameter; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link GenericParameterService}. + */ +class GenericParameterServiceTest { + @TempDir + private File tempDir; + + @Mock + private PropertyResolverUtils propertyResolverUtils; + + @Mock + private DelegatingMethodParameterCustomizer delegatingMethodParameterCustomizer; + + @Mock + private WebConversionServiceProvider webConversionServiceProvider; + + @Mock + private ObjectMapperProvider objectMapperProvider; + + @Mock + private JavadocProvider javadocProvider; + + private GenericParameterService genericParameterService; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + this.genericParameterService = new GenericParameterService(propertyResolverUtils, Optional.of(delegatingMethodParameterCustomizer), Optional.of(webConversionServiceProvider), objectMapperProvider, Optional.of(javadocProvider)); + } + + /** + * Tests for {@link GenericParameterService#getParamJavadoc(JavadocProvider, MethodParameter)}. + */ + @Nested + class getParamJavadoc { + @Mock + private DelegatingMethodParameter methodParameter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_17) + void hasDescriptionOfRecordObject() throws IOException, ClassNotFoundException, NoSuchMethodException { + Class cls = createRecordObject(); + Method method = cls.getMethod("name"); + + when(methodParameter.getParameterName()).thenReturn("name"); + when(methodParameter.isParameterObject()).thenReturn(true); + when(methodParameter.getExecutable()).thenReturn(method); + + Map recordParamMap = new HashMap<>(); + recordParamMap.put("id", "the id"); + recordParamMap.put("name", "the name"); + when(javadocProvider.getRecordClassParamJavadoc(cls)).thenReturn(recordParamMap); + + when(javadocProvider.getFieldJavadoc(any())).thenReturn(null); + + String actual = genericParameterService.getParamJavadoc(javadocProvider, methodParameter); + assertEquals("the name", actual); + + verify(methodParameter).getParameterName(); + verify(methodParameter).isParameterObject(); + verify(methodParameter).getExecutable(); + verify(javadocProvider).getRecordClassParamJavadoc(cls); + verify(javadocProvider).getFieldJavadoc(any()); + } + + @Test + void hasDescriptionOfClassObject() throws IOException, ClassNotFoundException, NoSuchMethodException { + Class cls = ClassObject.class; + Method method = cls.getMethod("getName"); + + when(methodParameter.getParameterName()).thenReturn("name"); + when(methodParameter.isParameterObject()).thenReturn(true); + when(methodParameter.getExecutable()).thenReturn(method); + + when(javadocProvider.getFieldJavadoc(any())).thenReturn("the name"); + + String actual = genericParameterService.getParamJavadoc(javadocProvider, methodParameter); + assertEquals("the name", actual); + + verify(methodParameter).getParameterName(); + verify(methodParameter).isParameterObject(); + verify(methodParameter).getExecutable(); + verify(javadocProvider).getFieldJavadoc(any()); + } + + private Class createRecordObject() throws IOException, ClassNotFoundException { + File recordObject = new File(tempDir, "RecordObject.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(recordObject))) { + writer.println("public record RecordObject(String id, String name){"); + writer.println("}"); + } + String[] args = { + recordObject.getAbsolutePath() + }; + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + int r = compiler.run(null, null, null, args); + if (r != 0) { + throw new IllegalStateException("Compilation failed"); + } + URL[] urls = { tempDir.toURI().toURL() }; + ClassLoader loader = URLClassLoader.newInstance(urls); + + return loader.loadClass("RecordObject"); + } + + private class ClassObject { + /** + * the id + */ + private String id; + + /** + * the name + */ + private String name; + + public ClassObject(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } + } +} diff --git a/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizer.java b/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizer.java index e86a7243c..a7f029e84 100644 --- a/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizer.java +++ b/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizer.java @@ -2,7 +2,7 @@ * * * * * * - * * * * Copyright 2019-2022 the original author or authors. + * * * * Copyright 2019-2023 the original author or authors. * * * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * * * you may not use this file except in compliance with the License. @@ -109,13 +109,23 @@ else if (resolvedSchema != null && resolvedSchema.get$ref() != null && resolvedS * @param fields the fields * @param existingSchema the existing schema */ - private void setJavadocDescription(Class cls, List fields, Schema existingSchema) { + void setJavadocDescription(Class cls, List fields, Schema existingSchema) { if (existingSchema != null) { if (StringUtils.isBlank(existingSchema.getDescription())) { existingSchema.setDescription(javadocProvider.getClassJavadoc(cls)); } Map properties = existingSchema.getProperties(); - if (!CollectionUtils.isEmpty(properties)) + if (!CollectionUtils.isEmpty(properties)) { + if (cls.getSuperclass() != null && "java.lang.Record".equals(cls.getSuperclass().getName())) { + Map recordParamMap = javadocProvider.getRecordClassParamJavadoc(cls); + properties.entrySet().stream() + .filter(stringSchemaEntry -> StringUtils.isBlank(stringSchemaEntry.getValue().getDescription())) + .forEach(stringSchemaEntry -> { + if (recordParamMap.containsKey(stringSchemaEntry.getKey())) + stringSchemaEntry.getValue().setDescription(recordParamMap.get(stringSchemaEntry.getKey())); + }); + } + properties.entrySet().stream() .filter(stringSchemaEntry -> StringUtils.isBlank(stringSchemaEntry.getValue().getDescription())) .forEach(stringSchemaEntry -> { @@ -126,6 +136,7 @@ private void setJavadocDescription(Class cls, List fields, Schema exis stringSchemaEntry.getValue().setDescription(fieldJavadoc); }); }); + } fields.stream().filter(f -> f.isAnnotationPresent(JsonUnwrapped.class)) .forEach(f -> setJavadocDescription(f.getType(), FieldUtils.getAllFieldsList(f.getType()), existingSchema)); diff --git a/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/SpringDocJavadocProvider.java b/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/SpringDocJavadocProvider.java index 4b7a2cf23..a4a39fa38 100644 --- a/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/SpringDocJavadocProvider.java +++ b/springdoc-openapi-javadoc/src/main/java/org/springdoc/openapi/javadoc/SpringDocJavadocProvider.java @@ -2,7 +2,7 @@ * * * * * * - * * * * Copyright 2019-2022 the original author or authors. + * * * * Copyright 2019-2023 the original author or authors. * * * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * * * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import com.github.therapi.runtimejavadoc.ClassJavadoc; import com.github.therapi.runtimejavadoc.CommentFormatter; @@ -64,6 +65,19 @@ public String getClassJavadoc(Class cl) { return formatter.format(classJavadoc.getComment()); } + /** + * Gets param descripton of record class. + * + * @param cl the class + * @return map of field and param descriptions + */ + @Override + public Map getRecordClassParamJavadoc(Class cl) { + ClassJavadoc classJavadoc = RuntimeJavadoc.getJavadoc(cl); + return classJavadoc.getRecordComponents().stream() + .collect(Collectors.toMap(ParamJavadoc::getName, record -> formatter.format(record.getComment()))); + } + /** * Gets method javadoc description. * diff --git a/springdoc-openapi-javadoc/src/test/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizerTest.java b/springdoc-openapi-javadoc/src/test/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizerTest.java new file mode 100644 index 000000000..855852ff4 --- /dev/null +++ b/springdoc-openapi-javadoc/src/test/java/org/springdoc/openapi/javadoc/JavadocPropertyCustomizerTest.java @@ -0,0 +1,134 @@ +/* + * + * * + * * * + * * * * Copyright 2019-2023 the original author or authors. + * * * * + * * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * * you may not use this file except in compliance with the License. + * * * * You may obtain a copy of the License at + * * * * + * * * * https://www.apache.org/licenses/LICENSE-2.0 + * * * * + * * * * Unless required by applicable law or agreed to in writing, software + * * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * * See the License for the specific language governing permissions and + * * * * limitations under the License. + * * * + * * + * + */ + +package org.springdoc.openapi.javadoc; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; + +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springdoc.core.providers.JavadocProvider; +import org.springdoc.core.providers.ObjectMapperProvider; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link JavadocPropertyCustomizer}. + */ +class JavadocPropertyCustomizerTest { + @TempDir + private File tempDir; + + private JavadocProvider javadocProvider; + + @Mock + private ObjectMapperProvider objectMapperProvider; + + private JavadocPropertyCustomizer javadocPropertyCustomizer; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + this.javadocProvider = new SpringDocJavadocProvider(); + this.javadocPropertyCustomizer = new JavadocPropertyCustomizer(javadocProvider, objectMapperProvider); + } + + + /** + * Tests for {@link JavadocPropertyCustomizer#setJavadocDescription(Class, List, Schema)}. + */ + @Nested + class setJavadocDescription { + @Test + @EnabledForJreRange(min = JRE.JAVA_17) + void ifRecordObjectShouldGetField() throws IOException, ClassNotFoundException { + File recordObject = new File(tempDir, "RecordObject.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(recordObject))) { + writer.println("/**"); + writer.println(" * Reord Object"); + writer.println(" *"); + writer.println(" * @param id the id"); + writer.println(" * @param name the name"); + writer.println(" */"); + writer.println("public record RecordObject(String id, String name){"); + writer.println("}"); + } + File recordObjectJavadocJson = new File(tempDir, "RecordObject__Javadoc.json"); + try (PrintWriter writer = new PrintWriter(new FileWriter(recordObjectJavadocJson))) { + writer.print("{"); + writer.print("\"doc\":\"Record Object\\n\\n @param id the id\\n @param name the name\","); + writer.print("\"fields\":[],"); + writer.print("\"methods\":[],"); + writer.print("\"constructors\":[]"); + writer.println("}"); + } + + String[] args = { + recordObject.getAbsolutePath() + }; + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + int r = compiler.run(null, null, null, args); + if (r != 0) { + throw new IllegalStateException("Compilation failed"); + } + URL[] urls = { tempDir.toURI().toURL() }; + ClassLoader loader = URLClassLoader.newInstance(urls); + + Class cls = loader.loadClass("RecordObject"); + + List fields = Arrays.asList(cls.getFields()); + + Schema existingSchema = new ObjectSchema().name("RecordObject") + .addProperty("id", new StringSchema().name("id")) + .addProperty("name", new StringSchema().name("name")); + + javadocPropertyCustomizer.setJavadocDescription(cls, fields, existingSchema); + + assertEquals("Record Object", existingSchema.getDescription()); + Map properties = existingSchema.getProperties(); + assertEquals(2, properties.size()); + assertEquals("the id", properties.get("id").getDescription()); + assertEquals("the name", properties.get("name").getDescription()); + } + } +}