diff --git a/make/Docs.gmk b/make/Docs.gmk index 5f10d623211..60c029ce8f9 100644 --- a/make/Docs.gmk +++ b/make/Docs.gmk @@ -79,6 +79,8 @@ JAVADOC_TAGS := \ -tag see \ -taglet build.tools.taglet.ExtLink \ -taglet build.tools.taglet.Incubating \ + -taglet build.tools.taglet.PreviewNote \ + --preview-note-tag previewNote \ -tagletpath $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ $(CUSTOM_JAVADOC_TAGS) \ # diff --git a/make/jdk/src/classes/build/tools/taglet/PreviewNote.java b/make/jdk/src/classes/build/tools/taglet/PreviewNote.java new file mode 100644 index 00000000000..ee3f9bea527 --- /dev/null +++ b/make/jdk/src/classes/build/tools/taglet/PreviewNote.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package build.tools.taglet; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + + +import javax.lang.model.element.Element; +import javax.tools.Diagnostic; + + +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.UnknownInlineTagTree; +import jdk.javadoc.doclet.Doclet; +import jdk.javadoc.doclet.DocletEnvironment; +import jdk.javadoc.doclet.Reporter; +import jdk.javadoc.doclet.StandardDoclet; +import jdk.javadoc.doclet.Taglet; + +import static com.sun.source.doctree.DocTree.Kind.UNKNOWN_INLINE_TAG; + +/** + * An inline tag to insert a note formatted as preview note. + * The tag can be used as follows: + * + *
+ * {@previewNote jep-number [Preview note heading]}
+ * Preview note content
+ * {@previewNote}
+ * 
+ * + */ +public class PreviewNote implements Taglet { + + static final String TAG_NAME = "previewNote"; + Reporter reporter = null; + + @Override + public void init(DocletEnvironment env, Doclet doclet) { + if (doclet instanceof StandardDoclet stdoclet) { + reporter = stdoclet.getReporter(); + } + } + + /** + * Returns the set of locations in which the tag may be used. + */ + @Override + public Set getAllowedLocations() { + return EnumSet.allOf(Taglet.Location.class); + } + + @Override + public boolean isInlineTag() { + return true; + } + + @Override + public String getName() { + return TAG_NAME; + } + + @Override + public String toString(List tags, Element elem) { + + for (DocTree tag : tags) { + if (tag.getKind() == UNKNOWN_INLINE_TAG) { + UnknownInlineTagTree inlineTag = (UnknownInlineTagTree) tag; + String[] content = inlineTag.getContent().toString().trim().split("\\s+", 2); + if (!content[0].isBlank()) { + StringBuilder sb = new StringBuilder(""" +
+ """); + if (content.length == 2) { + sb.append(""" +
+ """) + .append(content[1]) + .append(""" +
+ """); + } + sb.append(""" +
+ """); + return sb.toString(); + } else { + return """ +
+
+ """; + } + } + } + + if (reporter == null) { + throw new IllegalArgumentException("@" + TAG_NAME + " taglet content must be begin or end"); + } + reporter.print(Diagnostic.Kind.ERROR, "@" + TAG_NAME + " taglet content must be begin or end"); + return ""; + } +} diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/PreviewListWriter.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/PreviewListWriter.java index 4fb0d427ff9..b1c1bc42c58 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/PreviewListWriter.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/PreviewListWriter.java @@ -38,6 +38,7 @@ import jdk.javadoc.internal.doclets.toolkit.util.DocPaths; import jdk.javadoc.internal.doclets.toolkit.util.PreviewAPIListBuilder; import jdk.javadoc.internal.html.Content; import jdk.javadoc.internal.html.ContentBuilder; +import jdk.javadoc.internal.html.HtmlId; import jdk.javadoc.internal.html.HtmlStyle; import jdk.javadoc.internal.html.HtmlTree; import jdk.javadoc.internal.html.Text; @@ -96,6 +97,22 @@ public class PreviewListWriter extends SummaryListWriter } } + @Override + protected void addExtraSection(Content content) { + var notes = builder.getElementNotes(); + if (!notes.isEmpty()) { + addSummaryAPI(notes, HtmlId.of("preview-api-notes"), + "doclet.Preview_Notes_Elements", "doclet.Element", content); + } + } + + @Override + protected void addExtraIndexLink(Content target) { + if (!builder.getElementNotes().isEmpty()) { + addIndexLink(HtmlId.of("preview-api-notes"), "doclet.Preview_Notes", target); + } + } + @Override protected void addComments(Element e, Content desc) { List tags = utils.getFirstSentenceTrees(e); diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties index e4365b04cab..a713179a487 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties @@ -136,6 +136,8 @@ doclet.Preview_API_Checkbox_Toggle_All=Toggle all doclet.Preview_JEP_URL=https://openjdk.org/jeps/{0} doclet.Preview_Label=Preview doclet.Preview_Mark=PREVIEW +doclet.Preview_Notes=Preview API Notes +doclet.Preview_Notes_Elements=Elements containing Preview Notes doclet.Restricted_Methods=Restricted Methods doclet.Restricted_Mark=RESTRICTED doclet.searchTag=Search Tag diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/BaseOptions.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/BaseOptions.java index a1533eb115a..be698d270ae 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/BaseOptions.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/BaseOptions.java @@ -236,6 +236,14 @@ public abstract class BaseOptions { */ private boolean noTimestamp = false; + + /** + * Argument for command-line option {@code --preview-note-tag}. + * If set, the JavaDoc inline tag with the given name is considered + * a preview note and added to the preview API page. + */ + private String previewNoteTag = null; + /** * Argument for command-line option {@code -quiet}. * Suppress all messages @@ -549,6 +557,14 @@ public abstract class BaseOptions { } }, + new Hidden(resources, "--preview-note-tag", 1) { + @Override + public boolean process(String option, List args) { + previewNoteTag = args.getFirst(); + return true; + } + }, + new Hidden(resources, "-quiet") { @Override public boolean process(String opt, List args) { @@ -940,6 +956,12 @@ public abstract class BaseOptions { return noTimestamp; } + /** + * Argument for command-line option {@code --preview-note-tag}. + * Name of inline tag for preview notes. + */ + public String previewNoteTag() { return previewNoteTag; } + /** * Argument for command-line option {@code -quiet}. * Suppress all messages diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/WorkArounds.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/WorkArounds.java index daf0aabab27..b1266516586 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/WorkArounds.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/WorkArounds.java @@ -396,34 +396,6 @@ public class WorkArounds { return compilerOptions.isSet("accessInternalAPI"); } - /** - * Returns a map containing {@code jdk.internal.javac.PreviewFeature.JEP} element values associated with the - * {@code jdk.internal.javac.PreviewFeature.Feature} enum constant identified by {@code feature}. - * - * This method uses internal javac features (although only reflectively). - * - * @param feature the name of the PreviewFeature.Feature enum value - * @return the map of PreviewFeature.JEP annotation element values, or an empty map - */ - public Map getJepInfo(String feature) { - TypeElement featureType = elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.Feature"); - TypeElement jepType = elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.JEP"); - var featureVar = featureType.getEnclosedElements().stream() - .filter(e -> feature.equals(e.getSimpleName().toString())).findFirst(); - if (featureVar.isPresent()) { - for (AnnotationMirror anno : featureVar.get().getAnnotationMirrors()) { - if (anno.getAnnotationType().asElement().equals(jepType)) { - return anno.getElementValues().entrySet() - .stream() - .collect(Collectors.toMap( - e -> e.getKey().getSimpleName().toString(), - e -> e.getValue().getValue())); - } - } - } - return Map.of(); - } - /* * If a similar query is ever added to javax.lang.model, use that instead. */ diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/PreviewAPIListBuilder.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/PreviewAPIListBuilder.java index e9e92c13e8f..4c43e40eca6 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/PreviewAPIListBuilder.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/PreviewAPIListBuilder.java @@ -25,15 +25,22 @@ package jdk.javadoc.internal.doclets.toolkit.util; +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.UnknownInlineTagTree; import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.SortedSet; import java.util.TreeSet; -import java.util.stream.Collectors; /** * Build list of all the preview packages, classes, constructors, fields and methods. @@ -41,8 +48,9 @@ import java.util.stream.Collectors; public class PreviewAPIListBuilder extends SummaryAPIListBuilder { private final Map elementJeps = new HashMap<>(); + private final SortedSet elementNotes = createSummarySet(); private final Map jeps = new HashMap<>(); - private static final JEP NULL_SENTINEL = new JEP(0, "", ""); + private final String previewNoteTag; /** * The JEP for a preview feature in this release. @@ -61,39 +69,78 @@ public class PreviewAPIListBuilder extends SummaryAPIListBuilder { */ public PreviewAPIListBuilder(BaseConfiguration configuration) { super(configuration); - buildSummaryAPIInfo(); + this.previewNoteTag = configuration.getOptions().previewNoteTag(); + // retrieve preview JEPs + buildPreviewFeatureInfo(); + if (!jeps.isEmpty()) { + // map elements to preview JEPs and preview tags + buildSummaryAPIInfo(); + // remove unused preview JEPs + jeps.entrySet().removeIf(e -> !elementJeps.containsValue(e.getValue())); + } + } + + private void buildPreviewFeatureInfo() { + TypeElement featureType = utils.elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.Feature"); + if (featureType == null) { + return; + } + TypeElement jepType = utils.elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.JEP"); + featureType.getEnclosedElements().forEach(elem -> { + for (AnnotationMirror anno : elem.getAnnotationMirrors()) { + if (anno.getAnnotationType().asElement().equals(jepType)) { + Map values = anno.getElementValues(); + jeps.put(elem.getSimpleName().toString(), new JEP( + getAnnotationElementValue(values, "number", 0), + getAnnotationElementValue(values, "title", ""), + getAnnotationElementValue(values, "status", "Preview")) + ); + } + } + }); + } + + // Extract a single annotation element value with the given name and default value + @SuppressWarnings("unchecked") + private R getAnnotationElementValue(Map values, + String name, R defaultValue) { + Optional value = values.entrySet().stream() + .filter(e -> Objects.equals(e.getKey().getSimpleName().toString(), name)) + .map(e -> (R) e.getValue().getValue()) + .findFirst(); + return value.orElse(defaultValue); } @Override protected boolean belongsToSummary(Element element) { - if (!utils.isPreviewAPI(element)) { - return false; - } - String feature = Objects.requireNonNull(utils.getPreviewFeature(element), - "Preview feature not specified").toString(); - JEP jep = jeps.computeIfAbsent(feature, featureName -> { - Map jepInfo = configuration.workArounds.getJepInfo(featureName); - if (!jepInfo.isEmpty()) { - int number = 0; - String title = ""; - String status = "Preview"; // Default value is not returned by the method we used above. - for (var entry : jepInfo.entrySet()) { - switch (entry.getKey()) { - case "number" -> number = (int) entry.getValue(); - case "title" -> title = (String) entry.getValue(); - case "status" -> status = (String) entry.getValue(); - default -> throw new IllegalArgumentException(entry.getKey()); - } - } - return new JEP(number, title, status); + if (utils.isPreviewAPI(element)) { + String feature = Objects.requireNonNull(utils.getPreviewFeature(element), + "Preview feature not specified").toString(); + // Preview features without JEP are not included in the list. + JEP jep = jeps.get(feature); + if (jep != null) { + elementJeps.put(element, jep); + return true; + } + } + // If preview tag is defined map elements to preview tags + if (previewNoteTag != null) { + CommentHelper ch = utils.getCommentHelper(element); + if (ch.dcTree != null) { + var jep = ch.dcTree.getFullBody().stream() + .filter(dt -> dt.getKind() == DocTree.Kind.UNKNOWN_INLINE_TAG) + .map(dt -> (UnknownInlineTagTree) dt) + .filter(t -> previewNoteTag.equals(t.getTagName()) && !t.getContent().isEmpty()) + .map(this::findJEP) + .filter(Objects::nonNull) + .findFirst(); + if (jep.isPresent()) { + elementNotes.add(element); + elementJeps.put(element, jep.get()); + // Don't return true as this is not actual preview API. + } } - return NULL_SENTINEL; - }); - if (jep != NULL_SENTINEL) { - elementJeps.put(element, jep); - return true; } - // Preview features without JEP are not included. return false; } @@ -101,10 +148,7 @@ public class PreviewAPIListBuilder extends SummaryAPIListBuilder { * {@return a sorted set of preview feature JEPs in this release} */ public Set getJEPs() { - return jeps.values() - .stream() - .filter(jep -> jep != NULL_SENTINEL) - .collect(Collectors.toCollection(TreeSet::new)); + return new TreeSet<>(jeps.values()); } /** @@ -113,4 +157,26 @@ public class PreviewAPIListBuilder extends SummaryAPIListBuilder { public JEP getJEP(Element e) { return elementJeps.get(e); } + + /** + * {@return a sorted set containing elements tagged with preview notes} + */ + public SortedSet getElementNotes() { + return elementNotes; + } + + private JEP findJEP(UnknownInlineTagTree tag) { + var content = tag.getContent().toString().trim().split("\\s+", 2); + try { + var jnum = Integer.parseInt(content[0]); + for (var jep : jeps.values()) { + if (jep.number == jnum) { + return jep; + } + } + } catch (NumberFormatException nfe) { + // print warning? + } + return null; + } } diff --git a/test/langtools/jdk/javadoc/doclet/testPreview/TestPreview.java b/test/langtools/jdk/javadoc/doclet/testPreview/TestPreview.java index 55c6e541ad7..07e5b69c463 100644 --- a/test/langtools/jdk/javadoc/doclet/testPreview/TestPreview.java +++ b/test/langtools/jdk/javadoc/doclet/testPreview/TestPreview.java @@ -24,7 +24,7 @@ /* * @test * @bug 8250768 8261976 8277300 8282452 8287597 8325325 8325874 8297879 - * 8331947 8281533 8343239 8318416 + * 8331947 8281533 8343239 8318416 8346109 * @summary test generated docs for items declared using preview * @library /tools/lib ../../lib * @modules jdk.javadoc/jdk.javadoc.internal.tool @@ -296,4 +296,54 @@ public class TestPreview extends JavadocTester { checkOutput("m/module-summary.html", true, "Indirect exports from the java.base module are"); } + + // Test for JDK hidden option to add an entry for a non-preview element + // in the preview page based on the presence of a javadoc tag. + @Test + public void testPreviewNoteTag(Path base) throws IOException { + Path src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package p; + import jdk.internal.javac.PreviewFeature; + + /** + * Preview feature + */ + @PreviewFeature(feature= PreviewFeature.Feature.TEST) + public interface CoreInterface { + } + """, """ + package p; + + /** + * Non preview feature. + * {@previewNote 2147483647 Preview API Note} + */ + public interface NonPrevieFeature { + } + """); + javadoc("-d", "out-preview-note-tag", + "--add-exports", "java.base/jdk.internal.javac=ALL-UNNAMED", + "-tag", "previewNote:a:Preview Note:", + "--preview-note-tag", "previewNote", + "--source-path", + src.toString(), + "p"); + checkExit(Exit.OK); + + checkOutput("preview-list.html", true, + """ +

Contents

+