From e1f75dee639b01c795a8f063a74d03a9c0f53920 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 5 Sep 2022 00:18:01 +0900 Subject: [PATCH] Support Java record for v1.6. Fixes #1830 --- .../core/MethodParameterPojoExtractor.java | 44 +++++-- .../MethodParameterPojoExtractorTest.java | 122 ++++++++++++++++++ 2 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 springdoc-openapi-common/src/test/java/org/springdoc/core/MethodParameterPojoExtractorTest.java diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/MethodParameterPojoExtractor.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/MethodParameterPojoExtractor.java index 93d53e503..8edfdd6b2 100644 --- a/springdoc-openapi-common/src/main/java/org/springdoc/core/MethodParameterPojoExtractor.java +++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/MethodParameterPojoExtractor.java @@ -25,9 +25,7 @@ import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; +import java.lang.reflect.*; import java.nio.charset.Charset; import java.time.Duration; import java.time.LocalTime; @@ -175,17 +173,39 @@ private static Stream fromSimpleClass(Class paramClass, Fiel Annotation[] fieldAnnotations = field.getDeclaredAnnotations(); try { Parameter parameter = field.getAnnotation(Parameter.class); - boolean isNotRequired = parameter == null || !parameter.required(); + boolean isNotRequired = parameter == null || !parameter.required(); Annotation[] finalFieldAnnotations = fieldAnnotations; - return Stream.of(Introspector.getBeanInfo(paramClass).getPropertyDescriptors()) - .filter(d -> d.getName().equals(field.getName())) - .map(PropertyDescriptor::getReadMethod) - .filter(Objects::nonNull) - .map(method -> new MethodParameter(method, -1)) - .map(methodParameter -> DelegatingMethodParameter.changeContainingClass(methodParameter, paramClass)) - .map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), finalFieldAnnotations, true, isNotRequired)); + + if ("java.lang.Record".equals(paramClass.getSuperclass().getName())) { + Method classGetRecordComponents = Class.class.getMethod("getRecordComponents"); + Object[] components = (Object[]) classGetRecordComponents.invoke(paramClass); + + Class c = Class.forName("java.lang.reflect.RecordComponent"); + Method recordComponentGetAccessor = c.getMethod("getAccessor"); + + List methods = new ArrayList<>(); + for (Object object : components) { + methods.add((Method) recordComponentGetAccessor.invoke(object)); + } + return methods.stream() + .filter(method -> method.getName().equals(field.getName())) + .map(method -> new MethodParameter(method, -1)) + .map(methodParameter -> DelegatingMethodParameter.changeContainingClass(methodParameter, paramClass)) + .map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), finalFieldAnnotations, true, isNotRequired)); + + } + else + return Stream.of(Introspector.getBeanInfo(paramClass).getPropertyDescriptors()) + .filter(d -> d.getName().equals(field.getName())) + .map(PropertyDescriptor::getReadMethod) + .filter(Objects::nonNull) + .map(method -> new MethodParameter(method, -1)) + .map(methodParameter -> DelegatingMethodParameter.changeContainingClass(methodParameter, paramClass)) + .map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), finalFieldAnnotations, true, isNotRequired)); } - catch (IntrospectionException e) { + catch (IntrospectionException | NoSuchMethodException | + InvocationTargetException | IllegalAccessException | + ClassNotFoundException e) { return Stream.of(); } } diff --git a/springdoc-openapi-common/src/test/java/org/springdoc/core/MethodParameterPojoExtractorTest.java b/springdoc-openapi-common/src/test/java/org/springdoc/core/MethodParameterPojoExtractorTest.java new file mode 100644 index 000000000..4748f155d --- /dev/null +++ b/springdoc-openapi-common/src/test/java/org/springdoc/core/MethodParameterPojoExtractorTest.java @@ -0,0 +1,122 @@ +/* + * + * * + * * * + * * * * Copyright 2019-2022 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 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.springframework.core.MethodParameter; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; + +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.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MethodParameterPojoExtractor}. + */ +class MethodParameterPojoExtractorTest { + @TempDir + File tempDir; + + /** + * Tests for {@link MethodParameterPojoExtractor#extractFrom(Class)}. + */ + @Nested + class extractFrom { + @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("public record RecordObject(String email, String firstName, String lastName){"); + 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 clazz = loader.loadClass("RecordObject"); + + Stream actual = MethodParameterPojoExtractor.extractFrom(clazz); + assertThat(actual) + .extracting(MethodParameter::getMethod) + .extracting(Method::getName) + .containsOnlyOnce("email", "firstName", "lastName"); + } + + @Test + void ifClassObjectShouldGetMethod() { + Stream actual = MethodParameterPojoExtractor.extractFrom(ClassObject.class); + assertThat(actual) + .extracting(MethodParameter::getMethod) + .extracting(Method::getName) + .containsOnlyOnce("getEmail", "getFirstName", "getLastName"); + } + + public class ClassObject { + private String email; + + private String firstName; + + private String lastName; + + public ClassObject(String email, String firstName, String lastName) { + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + } + } +}