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 extends DocTree> 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("""
+
+
+ """;
+ }
+ }
+ }
+
+ 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 extends DocTree> 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 extends ExecutableElement, ? extends AnnotationValue> 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 extends ExecutableElement, ? extends AnnotationValue> 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
+
+ - Preview API Notes
+ - Interfaces
""",
+ """
+ Elements containing Preview Notes
""",
+ """
+
+ Test Feature
+
+
Non preview feature.
""");
+ }
}