From 8e8a0414d99aef6e82ee069951ff3b58e2f22d0c Mon Sep 17 00:00:00 2001 From: fdutton Date: Mon, 19 Apr 2021 03:57:53 -0400 Subject: [PATCH] #432 Adds support for Java 14 records (#437) --- spring-auto-restdocs-json-doclet-jdk9/pom.xml | 94 +++++++++++++----- .../jsondoclet/ClassDocumentation.java | 51 ++++++++-- .../jsondoclet/FieldDocumentation.java | 12 ++- .../ExtractDocumentationAsJsonDocletTest.java | 81 +++++++++++++--- .../restdocs/jsondoclet/RecordTest.java | 96 +++++++++++++++++++ .../restdocs/jsondoclet/DocumentedClass.java | 0 6 files changed, 287 insertions(+), 47 deletions(-) create mode 100644 spring-auto-restdocs-json-doclet-jdk9/src/test/java14/capital/scalable/restdocs/jsondoclet/RecordTest.java rename spring-auto-restdocs-json-doclet-jdk9/src/test/{java => resources}/capital/scalable/restdocs/jsondoclet/DocumentedClass.java (100%) diff --git a/spring-auto-restdocs-json-doclet-jdk9/pom.xml b/spring-auto-restdocs-json-doclet-jdk9/pom.xml index ec9ceaef..5d96a1a8 100644 --- a/spring-auto-restdocs-json-doclet-jdk9/pom.xml +++ b/spring-auto-restdocs-json-doclet-jdk9/pom.xml @@ -16,6 +16,7 @@ Doclet exporting JavaDoc to JSON for Spring Auto REST Docs + true UTF-8 @@ -23,6 +24,16 @@ com.fasterxml.jackson.core jackson-databind + + [2.11.2,2.12.0),[2.12.1,2.99) org.apache.commons @@ -54,35 +65,70 @@ + org.apache.maven.plugins maven-compiler-plugin 9 - - maven-javadoc-plugin - true - - - generate-javadoc-json - compile - - test-javadoc-no-fork - - - capital.scalable.restdocs.jsondoclet.ExtractDocumentationAsJsonDoclet - - capital.scalable - spring-auto-restdocs-json-doclet-jdk9 - ${project.parent.version} - - ${project.build.directory} - false - package - - - - + + + + + java14 + + [14,17) + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + --enable-preview + --illegal-access=permit + + + alphabetical + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + test-compile-java-14 + test-compile + + testCompile + + + ${java.specification.version} + true + + + --enable-preview + + + ${project.basedir}/src/test/java14 + + + + + + + + + + + diff --git a/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/ClassDocumentation.java b/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/ClassDocumentation.java index 0389fe59..54dbf20d 100644 --- a/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/ClassDocumentation.java +++ b/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/ClassDocumentation.java @@ -19,12 +19,17 @@ */ package capital.scalable.restdocs.jsondoclet; +import static java.util.Optional.ofNullable; import static capital.scalable.restdocs.jsondoclet.DocletUtils.cleanupDocComment; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.ParamTree; + import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import jdk.javadoc.doclet.DocletEnvironment; @@ -42,14 +47,41 @@ public static ClassDocumentation fromClassDoc(DocletEnvironment docEnv, ClassDocumentation cd = new ClassDocumentation(); cd.setComment(cleanupDocComment(docEnv.getElementUtils().getDocComment(element))); - element.getEnclosedElements().forEach(fieldOrMethod -> { - if (fieldOrMethod.getKind().equals(ElementKind.FIELD)) { - cd.addField(docEnv, fieldOrMethod); - } else if (fieldOrMethod.getKind().equals(ElementKind.METHOD) - || fieldOrMethod.getKind().equals(ElementKind.CONSTRUCTOR)) { - cd.addMethod(docEnv, fieldOrMethod); - } - }); + if ("RECORD".equals(element.getKind().name())) { + ofNullable(docEnv.getDocTrees().getDocCommentTree(element)) + .stream() + .map(DocCommentTree::getBlockTags) + .flatMap(List::stream) + .filter(ParamTree.class::isInstance) + .map(ParamTree.class::cast) + .filter(p -> !p.isTypeParameter()) + .forEach(p -> { + String name = p.getName().getName().toString(); + String desc = p.getDescription() + .stream() + .map(Object::toString) + .collect(Collectors.joining(" ")) + ; + + cd.fields.put(name, FieldDocumentation.fromString(desc)); + }) + ; + } else { + element.getEnclosedElements().forEach(fieldOrMethod -> { + switch (fieldOrMethod.getKind()) { + case FIELD: + cd.addField(docEnv, fieldOrMethod); + break; + case METHOD: + case CONSTRUCTOR: + cd.addMethod(docEnv, fieldOrMethod); + break; + default: + // Ignored + break; + } + }); + } return cd; } @@ -67,4 +99,5 @@ private void addMethod(DocletEnvironment docEnv, Element element) { this.methods.put(element.getSimpleName().toString(), MethodDocumentation.fromMethodDoc(docEnv, element)); } + } diff --git a/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/FieldDocumentation.java b/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/FieldDocumentation.java index 12a0298e..f3224d4c 100644 --- a/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/FieldDocumentation.java +++ b/spring-auto-restdocs-json-doclet-jdk9/src/main/java/capital/scalable/restdocs/jsondoclet/FieldDocumentation.java @@ -37,6 +37,10 @@ public class FieldDocumentation { private String comment; private final Map tags = new HashMap<>(); + private FieldDocumentation(String comment) { + this.comment = comment; + } + private void addTag(DocTree tag) { if (tag instanceof BlockTagTree) { tags.put( @@ -47,8 +51,8 @@ private void addTag(DocTree tag) { public static FieldDocumentation fromFieldDoc(DocletEnvironment docEnv, Element fieldElement) { - FieldDocumentation fd = new FieldDocumentation(); - fd.comment = cleanupDocComment(docEnv.getElementUtils().getDocComment(fieldElement)); + FieldDocumentation fd = fromString( + cleanupDocComment(docEnv.getElementUtils().getDocComment(fieldElement))); Optional.ofNullable(docEnv.getDocTrees().getDocCommentTree(fieldElement)) .ifPresent(docCommentTree -> docCommentTree.getBlockTags() @@ -56,4 +60,8 @@ public static FieldDocumentation fromFieldDoc(DocletEnvironment docEnv, return fd; } + + public static FieldDocumentation fromString(String comment) { + return new FieldDocumentation(comment); + } } diff --git a/spring-auto-restdocs-json-doclet-jdk9/src/test/java/capital/scalable/restdocs/jsondoclet/ExtractDocumentationAsJsonDocletTest.java b/spring-auto-restdocs-json-doclet-jdk9/src/test/java/capital/scalable/restdocs/jsondoclet/ExtractDocumentationAsJsonDocletTest.java index 0915d8dc..873710a0 100644 --- a/spring-auto-restdocs-json-doclet-jdk9/src/test/java/capital/scalable/restdocs/jsondoclet/ExtractDocumentationAsJsonDocletTest.java +++ b/spring-auto-restdocs-json-doclet-jdk9/src/test/java/capital/scalable/restdocs/jsondoclet/ExtractDocumentationAsJsonDocletTest.java @@ -19,32 +19,89 @@ */ package capital.scalable.restdocs.jsondoclet; -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.File; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.List; + +import javax.tools.DocumentationTool.DocumentationTask; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; +import static org.junit.Assert.assertTrue; + +import static java.nio.charset.StandardCharsets.UTF_8; import org.apache.commons.io.IOUtils; import org.json.JSONException; +import org.junit.Before; import org.junit.Test; import org.skyscreamer.jsonassert.JSONAssert; public class ExtractDocumentationAsJsonDocletTest { + private static final Path SRC_PATH = FileSystems.getDefault().getPath("src/test/resources").toAbsolutePath(); + private static final Path TGT_PATH = FileSystems.getDefault().getPath("target/test/generated-javadoc-json").toAbsolutePath(); + private static final String JSON_PATH = "capital/scalable/restdocs/jsondoclet/DocumentedClass.json"; - /** - * The test requires that the Doclet is executed before. This is ensured by - * the Maven configuration, but not when the test is executed on its own. - */ + private static final List args = List.of( + "--release", "9", + "-private", + "-d", TGT_PATH.toString() + ); + + @Before + public void setup() throws IOException { + Files.createDirectories(TGT_PATH); + } + @Test public void testDocumentedClass() throws IOException, JSONException { - String generated = IOUtils.toString( - new FileInputStream(new File("target/generated-javadoc-json/" + JSON_PATH)), UTF_8); - String expected = IOUtils.toString( - this.getClass().getClassLoader().getResourceAsStream(JSON_PATH), UTF_8); - JSONAssert.assertEquals(expected, generated, false); + Path source = SRC_PATH.resolve("capital/scalable/restdocs/jsondoclet/DocumentedClass.java"); + Iterable compilationUnits = compilationUnits(source); + + DocumentationTask task = ToolProvider.getSystemDocumentationTool().getTask(null, null, null, ExtractDocumentationAsJsonDoclet.class, args, compilationUnits); + + boolean result = task.call(); + assertTrue(result); + + try ( + InputStream expectedStream = new FileInputStream(SRC_PATH.resolve(JSON_PATH).toFile()); + InputStream generatedStream = new FileInputStream(TGT_PATH.resolve(JSON_PATH).toFile()); + ) { + String expected = IOUtils.toString(expectedStream, UTF_8); + String generated = IOUtils.toString(generatedStream, UTF_8); + JSONAssert.assertEquals(expected, generated, false); + } + } + + Iterable compilationUnits(Path path) throws IOException { + try (InputStream in = new FileInputStream(path.toFile())) { + String content = IOUtils.toString(in, UTF_8); + return List.of(new InMemoryJavaFileObject(path.toUri(), content)); + } + } + + static class InMemoryJavaFileObject extends SimpleJavaFileObject { + + private final String source; + + public InMemoryJavaFileObject(URI uri, String sourceCode) { + super(uri, Kind.SOURCE); + this.source = sourceCode; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return source; + } + } + } diff --git a/spring-auto-restdocs-json-doclet-jdk9/src/test/java14/capital/scalable/restdocs/jsondoclet/RecordTest.java b/spring-auto-restdocs-json-doclet-jdk9/src/test/java14/capital/scalable/restdocs/jsondoclet/RecordTest.java new file mode 100644 index 00000000..2f5cfcb1 --- /dev/null +++ b/spring-auto-restdocs-json-doclet-jdk9/src/test/java14/capital/scalable/restdocs/jsondoclet/RecordTest.java @@ -0,0 +1,96 @@ +/*- + * #%L + * Spring Auto REST Docs Json Doclet for JDK9+ + * %% + * Copyright (C) 2015 - 2021 Scalable Capital GmbH + * %% + * 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 + * + * http://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. + * #L% + */ +package capital.scalable.restdocs.jsondoclet; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.io.IOException; +import java.net.URI; +import java.util.List; + +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.Before; +import org.junit.Test; + +public class RecordTest { + + private static final Path TGT_PATH = Path.of("target/test/generated-javadoc-json").toAbsolutePath(); + + private static final List args = List.of( + "--enable-preview", + "--release", Integer.toString(Runtime.version().feature()), + "-private", + "-d", TGT_PATH.toString() + ); + + @Before + public void setup() throws IOException { + Files.createDirectories(TGT_PATH); + } + + @Test + public void testDocumentedRecord() throws IOException { + var src = new InMemoryJavaFileObject(URI.create("string:///DocumentedRecord.java"), + """ + /** + * This is a test record. UTF 8 test: 我能吞下玻璃而不伤身体。Árvíztűrő tükörfúrógép + * + * @ Don't fail on invalid tags + * @param foo this is a record component + */ + record DocumentedRecord(int foo) {} + """ + ); + + var task = ToolProvider.getSystemDocumentationTool().getTask(null, null, null, ExtractDocumentationAsJsonDoclet.class, args, List.of(src)); + + boolean result = task.call(); + assertTrue(result); + + var mapper = new ObjectMapper(); + var tree = mapper.readTree(TGT_PATH.resolve("DocumentedRecord.json").toFile()); + assertEquals("This is a test record. UTF 8 test: 我能吞下玻璃而不伤身体。Árvíztűrő tükörfúrógép", tree.at("/comment").asText()); + assertEquals("this is a record component", tree.at("/fields/foo/comment").asText()); + + } + + static class InMemoryJavaFileObject extends SimpleJavaFileObject { + + private final String source; + + public InMemoryJavaFileObject(URI uri, String sourceCode) { + super(uri, Kind.SOURCE); + this.source = sourceCode; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return source; + } + + } + +} diff --git a/spring-auto-restdocs-json-doclet-jdk9/src/test/java/capital/scalable/restdocs/jsondoclet/DocumentedClass.java b/spring-auto-restdocs-json-doclet-jdk9/src/test/resources/capital/scalable/restdocs/jsondoclet/DocumentedClass.java similarity index 100% rename from spring-auto-restdocs-json-doclet-jdk9/src/test/java/capital/scalable/restdocs/jsondoclet/DocumentedClass.java rename to spring-auto-restdocs-json-doclet-jdk9/src/test/resources/capital/scalable/restdocs/jsondoclet/DocumentedClass.java