blob: 885288bbe3ca4a829a210e612389bd8c5cbc0b6b [file] [log] [blame]
/*
* Copyright (c) 2001, 2018, 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 jdk.javadoc.internal.doclets.toolkit.taglets;
import java.io.*;
import java.util.*;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.SimpleElementVisitor9;
import javax.tools.JavaFileManager;
import javax.tools.StandardJavaFileManager;
import com.sun.source.doctree.DocTree;
import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration;
import jdk.javadoc.internal.doclets.toolkit.DocletElement;
import jdk.javadoc.internal.doclets.toolkit.Messages;
import jdk.javadoc.internal.doclets.toolkit.Resources;
import jdk.javadoc.internal.doclets.toolkit.taglets.BaseTaglet.Site;
import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper;
import jdk.javadoc.internal.doclets.toolkit.util.Utils;
import static javax.tools.DocumentationTool.Location.*;
import static com.sun.source.doctree.DocTree.Kind.*;
/**
* Manages the {@code Taglet}s used by doclets.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*
* @author Jamie Ho
*/
public class TagletManager {
/**
* The default separator for the simple tag option.
*/
public static final char SIMPLE_TAGLET_OPT_SEPARATOR = ':';
/**
* The map of all taglets.
*/
private final LinkedHashMap<String,Taglet> allTaglets;
/**
* Block (non-line) taglets, grouped by Site
*/
private Map<Site, List<Taglet>> blockTagletsBySite;
/**
* The taglets that can appear inline in descriptive text.
*/
private List<Taglet> inlineTags;
/**
* The taglets that can appear in the serialized form.
*/
private List<Taglet> serializedFormTags;
private final DocletEnvironment docEnv;
private final Doclet doclet;
private final Utils utils;
private final Messages messages;
private final Resources resources;
/**
* Keep track of standard tags.
*/
private final Set<String> standardTags;
/**
* Keep track of standard tags in lowercase to compare for better
* error messages when a tag like @docRoot is mistakenly spelled
* lowercase @docroot.
*/
private final Set<String> standardTagsLowercase;
/**
* Keep track of overriden standard tags.
*/
private final Set<String> overridenStandardTags;
/**
* Keep track of the tags that may conflict
* with standard tags in the future (any custom tag without
* a period in its name).
*/
private final Set<String> potentiallyConflictingTags;
/**
* The set of unseen custom tags.
*/
private final Set<String> unseenCustomTags;
/**
* True if we do not want to use @since tags.
*/
private final boolean nosince;
/**
* True if we want to use @version tags.
*/
private final boolean showversion;
/**
* True if we want to use @author tags.
*/
private final boolean showauthor;
/**
* True if we want to use JavaFX-related tags (@defaultValue, @treatAsPrivate).
*/
private final boolean javafx;
/**
* Show the taglets table when it has been initialized.
*/
private final boolean showTaglets;
/**
* Construct a new {@code TagletManager}.
* @param nosince true if we do not want to use @since tags.
* @param showversion true if we want to use @version tags.
* @param showauthor true if we want to use @author tags.
* @param javafx indicates whether javafx is active.
* @param configuration the configuration for this taglet manager
*/
public TagletManager(boolean nosince, boolean showversion,
boolean showauthor, boolean javafx,
BaseConfiguration configuration) {
overridenStandardTags = new HashSet<>();
potentiallyConflictingTags = new HashSet<>();
standardTags = new HashSet<>();
standardTagsLowercase = new HashSet<>();
unseenCustomTags = new HashSet<>();
allTaglets = new LinkedHashMap<>();
this.nosince = nosince;
this.showversion = showversion;
this.showauthor = showauthor;
this.javafx = javafx;
this.docEnv = configuration.docEnv;
this.doclet = configuration.doclet;
this.messages = configuration.getMessages();
this.resources = configuration.getResources();
this.showTaglets = configuration.showTaglets;
this.utils = configuration.utils;
initStandardTaglets();
}
/**
* Add a new {@code Taglet}. This is used to add a Taglet from within
* a Doclet. No message is printed to indicate that the Taglet is properly
* registered because these Taglets are typically added for every execution of the
* Doclet. We don't want to see this type of error message every time.
* @param customTag the new {@code Taglet} to add.
*/
public void addCustomTag(Taglet customTag) {
if (customTag != null) {
String name = customTag.getName();
allTaglets.remove(name);
allTaglets.put(name, customTag);
checkTagName(name);
}
}
public Set<String> getAllTagletNames() {
return allTaglets.keySet();
}
/**
* Add a new {@code Taglet}. Print a message to indicate whether or not
* the Taglet was registered properly.
* @param classname the name of the class representing the custom tag.
* @param fileManager the filemanager to load classes and resources.
* @param tagletPath the path to the class representing the custom tag.
*/
public void addCustomTag(String classname, JavaFileManager fileManager, String tagletPath) {
try {
ClassLoader tagClassLoader;
if (!fileManager.hasLocation(TAGLET_PATH)) {
List<File> paths = new ArrayList<>();
if (tagletPath != null) {
for (String pathname : tagletPath.split(File.pathSeparator)) {
paths.add(new File(pathname));
}
}
if (fileManager instanceof StandardJavaFileManager) {
((StandardJavaFileManager) fileManager).setLocation(TAGLET_PATH, paths);
}
}
tagClassLoader = fileManager.getClassLoader(TAGLET_PATH);
Class<? extends jdk.javadoc.doclet.Taglet> customTagClass =
tagClassLoader.loadClass(classname).asSubclass(jdk.javadoc.doclet.Taglet.class);
jdk.javadoc.doclet.Taglet instance = customTagClass.getConstructor().newInstance();
instance.init(docEnv, doclet);
Taglet newLegacy = new UserTaglet(instance);
String tname = newLegacy.getName();
Taglet t = allTaglets.get(tname);
if (t != null) {
allTaglets.remove(tname);
}
allTaglets.put(tname, newLegacy);
messages.notice("doclet.Notice_taglet_registered", classname);
} catch (Exception exc) {
messages.error("doclet.Error_taglet_not_registered", exc.getClass().getName(), classname);
}
}
/**
* Add a new {@code SimpleTaglet}. If this tag already exists
* and the header passed as an argument is null, move tag to the back of the
* list. If this tag already exists and the header passed as an argument is
* not null, overwrite previous tag with new one. Otherwise, add new
* SimpleTaglet to list.
* @param tagName the name of this tag
* @param header the header to output.
* @param locations the possible locations that this tag
* can appear in.
*/
public void addNewSimpleCustomTag(String tagName, String header, String locations) {
if (tagName == null || locations == null) {
return;
}
Taglet tag = allTaglets.get(tagName);
if (tag == null || header != null) {
allTaglets.remove(tagName);
allTaglets.put(tagName, new SimpleTaglet(tagName, header, locations));
if (Utils.toLowerCase(locations).indexOf('x') == -1) {
checkTagName(tagName);
}
} else {
//Move to back
allTaglets.remove(tagName);
allTaglets.put(tagName, tag);
}
}
/**
* Given a tag name, add it to the set of tags it belongs to.
*/
private void checkTagName(String name) {
if (standardTags.contains(name)) {
overridenStandardTags.add(name);
} else {
if (name.indexOf('.') == -1) {
potentiallyConflictingTags.add(name);
}
unseenCustomTags.add(name);
}
}
/**
* Given a name of a seen custom tag, remove it from the set of unseen
* custom tags.
* @param name the name of the seen custom tag.
*/
void seenCustomTag(String name) {
unseenCustomTags.remove(name);
}
/**
* Given a series of {@code DocTree}s, check for spelling mistakes.
* @param element the tags holder
* @param trees the trees containing the comments
* @param areInlineTags true if the array of tags are inline and false otherwise.
*/
public void checkTags(Element element, Iterable<? extends DocTree> trees, boolean areInlineTags) {
if (trees == null) {
return;
}
CommentHelper ch = utils.getCommentHelper(element);
for (DocTree tag : trees) {
String name = tag.getKind().tagName;
if (name == null) {
continue;
}
if (name.length() > 0 && name.charAt(0) == '@') {
name = name.substring(1, name.length());
}
if (! (standardTags.contains(name) || allTaglets.containsKey(name))) {
if (standardTagsLowercase.contains(Utils.toLowerCase(name))) {
messages.warning(ch.getDocTreePath(tag), "doclet.UnknownTagLowercase", ch.getTagName(tag));
continue;
} else {
messages.warning(ch.getDocTreePath(tag), "doclet.UnknownTag", ch.getTagName(tag));
continue;
}
}
final Taglet taglet = allTaglets.get(name);
// Check and verify tag usage
if (taglet != null) {
if (areInlineTags && !taglet.isInlineTag()) {
printTagMisuseWarn(ch, taglet, tag, "inline");
}
// nothing more to do
if (element == null) {
return;
}
new SimpleElementVisitor9<Void, Void>() {
@Override
public Void visitModule(ModuleElement e, Void p) {
if (!taglet.inModule()) {
printTagMisuseWarn(utils.getCommentHelper(e), taglet, tag, "module");
}
return null;
}
@Override
public Void visitPackage(PackageElement e, Void p) {
if (!taglet.inPackage()) {
printTagMisuseWarn(utils.getCommentHelper(e), taglet, tag, "package");
}
return null;
}
@Override
public Void visitType(TypeElement e, Void p) {
if (!taglet.inType()) {
printTagMisuseWarn(utils.getCommentHelper(e), taglet, tag, "class");
}
return null;
}
@Override
public Void visitExecutable(ExecutableElement e, Void p) {
if (utils.isConstructor(e) && !taglet.inConstructor()) {
printTagMisuseWarn(utils.getCommentHelper(e), taglet, tag, "constructor");
} else if (!taglet.inMethod()) {
printTagMisuseWarn(utils.getCommentHelper(e), taglet, tag, "method");
}
return null;
}
@Override
public Void visitVariable(VariableElement e, Void p) {
if (utils.isField(e) && !taglet.inField()) {
printTagMisuseWarn(utils.getCommentHelper(e), taglet, tag, "field");
}
return null;
}
@Override
public Void visitUnknown(Element e, Void p) {
if (utils.isOverviewElement(e) && !taglet.inOverview()) {
printTagMisuseWarn(utils.getCommentHelper(e), taglet, tag, "overview");
}
return null;
}
@Override
protected Void defaultAction(Element e, Void p) {
return null;
}
}.visit(element);
}
}
}
/**
* Given the taglet, the tag and the type of documentation that the tag
* was found in, print a tag misuse warning.
* @param taglet the taglet representing the misused tag.
* @param tag the misused tag.
* @param holderType the type of documentation that the misused tag was found in.
*/
private void printTagMisuseWarn(CommentHelper ch, Taglet taglet, DocTree tag, String holderType) {
Set<String> locationsSet = new LinkedHashSet<>();
// The following names should be localized
if (taglet.inOverview()) {
locationsSet.add("overview");
}
if (taglet.inModule()) {
locationsSet.add("module");
}
if (taglet.inPackage()) {
locationsSet.add("package");
}
if (taglet.inType()) {
locationsSet.add("class/interface");
}
if (taglet.inConstructor()) {
locationsSet.add("constructor");
}
if (taglet.inField()) {
locationsSet.add("field");
}
if (taglet.inMethod()) {
locationsSet.add("method");
}
if (taglet.isInlineTag()) {
locationsSet.add("inline text");
}
if (locationsSet.isEmpty()) {
//This known tag is excluded.
return;
}
StringBuilder combined_locations = new StringBuilder();
for (String location: locationsSet) {
if (combined_locations.length() > 0) {
combined_locations.append(", ");
}
combined_locations.append(location);
}
messages.warning(ch.getDocTreePath(tag), "doclet.tag_misuse",
"@" + taglet.getName(), holderType, combined_locations.toString());
}
/**
* Returns the taglets that can appear inline, in descriptive text.
* @return the taglets that can appear inline
*/
List<Taglet> getInlineTaglets() {
if (inlineTags == null) {
initBlockTaglets();
}
return inlineTags;
}
/**
* Returns the taglets that can appear in the serialized form.
* @return the taglet that can appear in the serialized form
*/
public List<Taglet> getSerializedFormTaglets() {
if (serializedFormTags == null) {
initBlockTaglets();
}
return serializedFormTags;
}
/**
* Returns the custom tags for a given element.
*
* @param e the element to get custom tags for
* @return the array of {@code Taglet}s that can
* appear in the given element.
*/
@SuppressWarnings("fallthrough")
public List<Taglet> getBlockTaglets(Element e) {
if (blockTagletsBySite == null) {
initBlockTaglets();
}
switch (e.getKind()) {
case CONSTRUCTOR:
return blockTagletsBySite.get(Site.CONSTRUCTOR);
case METHOD:
return blockTagletsBySite.get(Site.METHOD);
case ENUM_CONSTANT:
case FIELD:
return blockTagletsBySite.get(Site.FIELD);
case ANNOTATION_TYPE:
case INTERFACE:
case CLASS:
case ENUM:
return blockTagletsBySite.get(Site.TYPE);
case MODULE:
return blockTagletsBySite.get(Site.MODULE);
case PACKAGE:
return blockTagletsBySite.get(Site.PACKAGE);
case OTHER:
if (e instanceof DocletElement) {
DocletElement de = (DocletElement)e;
switch (de.getSubKind()) {
case DOCFILE:
return blockTagletsBySite.get(Site.PACKAGE);
case OVERVIEW:
return blockTagletsBySite.get(Site.OVERVIEW);
default:
// fall through
}
}
// fall through
default:
throw new AssertionError("unknown element: " + e + " ,kind: " + e.getKind());
}
}
/**
* Initialize the custom tag Lists.
*/
private void initBlockTaglets() {
blockTagletsBySite = new EnumMap<>(Site.class);
for (Site site : Site.values()) {
blockTagletsBySite.put(site, new ArrayList<>());
}
inlineTags = new ArrayList<>();
for (Taglet current : allTaglets.values()) {
if (current.isInlineTag()) {
inlineTags.add(current);
} else {
if (current.inOverview()) {
blockTagletsBySite.get(Site.OVERVIEW).add(current);
}
if (current.inModule()) {
blockTagletsBySite.get(Site.MODULE).add(current);
}
if (current.inPackage()) {
blockTagletsBySite.get(Site.PACKAGE).add(current);
}
if (current.inType()) {
blockTagletsBySite.get(Site.TYPE).add(current);
}
if (current.inConstructor()) {
blockTagletsBySite.get(Site.CONSTRUCTOR).add(current);
}
if (current.inMethod()) {
blockTagletsBySite.get(Site.METHOD).add(current);
}
if (current.inField()) {
blockTagletsBySite.get(Site.FIELD).add(current);
}
}
}
//Init the serialized form tags
serializedFormTags = new ArrayList<>();
serializedFormTags.add(allTaglets.get(SERIAL_DATA.tagName));
serializedFormTags.add(allTaglets.get(THROWS.tagName));
if (!nosince)
serializedFormTags.add(allTaglets.get(SINCE.tagName));
serializedFormTags.add(allTaglets.get(SEE.tagName));
if (showTaglets) {
showTaglets(System.out);
}
}
/**
* Initialize standard Javadoc tags for ordering purposes.
*/
private void initStandardTaglets() {
if (javafx) {
initJavaFXTaglets();
}
addStandardTaglet(new ParamTaglet());
addStandardTaglet(new ReturnTaglet());
addStandardTaglet(new ThrowsTaglet());
addStandardTaglet(
new SimpleTaglet(EXCEPTION.tagName, null,
EnumSet.of(Site.METHOD, Site.CONSTRUCTOR)));
addStandardTaglet(
new SimpleTaglet(SINCE.tagName, resources.getText("doclet.Since"),
EnumSet.allOf(Site.class), !nosince));
addStandardTaglet(
new SimpleTaglet(VERSION.tagName, resources.getText("doclet.Version"),
EnumSet.of(Site.OVERVIEW, Site.MODULE, Site.PACKAGE, Site.TYPE), showversion));
addStandardTaglet(
new SimpleTaglet(AUTHOR.tagName, resources.getText("doclet.Author"),
EnumSet.of(Site.OVERVIEW, Site.MODULE, Site.PACKAGE, Site.TYPE), showauthor));
addStandardTaglet(
new SimpleTaglet(SERIAL_DATA.tagName, resources.getText("doclet.SerialData"),
EnumSet.noneOf(Site.class)));
addStandardTaglet(
new SimpleTaglet(HIDDEN.tagName, null,
EnumSet.of(Site.TYPE, Site.METHOD, Site.FIELD)));
// This appears to be a default custom (non-standard) taglet
Taglet factoryTaglet = new SimpleTaglet("factory", resources.getText("doclet.Factory"),
EnumSet.of(Site.METHOD));
allTaglets.put(factoryTaglet.getName(), factoryTaglet);
addStandardTaglet(new SeeTaglet());
// Standard inline tags
addStandardTaglet(new DocRootTaglet());
addStandardTaglet(new InheritDocTaglet());
addStandardTaglet(new ValueTaglet());
addStandardTaglet(new LiteralTaglet());
addStandardTaglet(new CodeTaglet());
addStandardTaglet(new IndexTaglet());
addStandardTaglet(new SummaryTaglet());
// Keep track of the names of standard tags for error checking purposes.
// The following are not handled above.
addStandardTaglet(new DeprecatedTaglet());
addStandardTaglet(new BaseTaglet(LINK.tagName, true, EnumSet.allOf(Site.class)));
addStandardTaglet(new BaseTaglet(LINK_PLAIN.tagName, true, EnumSet.allOf(Site.class)));
addStandardTaglet(new BaseTaglet(USES.tagName, false, EnumSet.of(Site.MODULE)));
addStandardTaglet(new BaseTaglet(PROVIDES.tagName, false, EnumSet.of(Site.MODULE)));
addStandardTaglet(
new SimpleTaglet(SERIAL.tagName, null,
EnumSet.of(Site.PACKAGE, Site.TYPE, Site.FIELD)));
addStandardTaglet(
new SimpleTaglet(SERIAL_FIELD.tagName, null, EnumSet.of(Site.FIELD)));
}
/**
* Initialize JavaFX-related tags.
*/
private void initJavaFXTaglets() {
addStandardTaglet(new PropertyGetterTaglet());
addStandardTaglet(new PropertySetterTaglet());
addStandardTaglet(new SimpleTaglet("propertyDescription",
resources.getText("doclet.PropertyDescription"),
EnumSet.of(Site.METHOD, Site.FIELD)));
addStandardTaglet(new SimpleTaglet("defaultValue", resources.getText("doclet.DefaultValue"),
EnumSet.of(Site.METHOD, Site.FIELD)));
addStandardTaglet(new SimpleTaglet("treatAsPrivate", null,
EnumSet.of(Site.TYPE, Site.METHOD, Site.FIELD)));
}
private void addStandardTaglet(Taglet taglet) {
String name = taglet.getName();
allTaglets.put(name, taglet);
standardTags.add(name);
standardTagsLowercase.add(Utils.toLowerCase(name));
}
public boolean isKnownCustomTag(String tagName) {
return allTaglets.containsKey(tagName);
}
/**
* Print a list of {@link Taglet}s that might conflict with
* standard tags in the future and a list of standard tags
* that have been overriden.
*/
public void printReport() {
printReportHelper("doclet.Notice_taglet_conflict_warn", potentiallyConflictingTags);
printReportHelper("doclet.Notice_taglet_overriden", overridenStandardTags);
printReportHelper("doclet.Notice_taglet_unseen", unseenCustomTags);
}
private void printReportHelper(String noticeKey, Set<String> names) {
if (names.size() > 0) {
StringBuilder result = new StringBuilder();
for (String name : names) {
result.append(result.length() == 0 ? " " : ", ");
result.append("@").append(name);
}
messages.notice(noticeKey, result);
}
}
/**
* Given the name of a tag, return the corresponding taglet.
* Return null if the tag is unknown.
*
* @param name the name of the taglet to retrieve.
* @return return the corresponding taglet. Return null if the tag is
* unknown.
*/
Taglet getTaglet(String name) {
if (name.indexOf("@") == 0) {
return allTaglets.get(name.substring(1));
} else {
return allTaglets.get(name);
}
}
/*
* The output of this method is the basis for a table at the end of the
* doc comment specification, so any changes in the output may indicate
* a need for a corresponding update to the spec.
*/
private void showTaglets(PrintStream out) {
Set<Taglet> taglets = new TreeSet<>((o1, o2) -> o1.getName().compareTo(o2.getName()));
taglets.addAll(allTaglets.values());
for (Taglet t : taglets) {
String name = t.isInlineTag() ? "{@" + t.getName() + "}" : "@" + t.getName();
out.println(String.format("%20s", name) + ": "
+ format(t.inOverview(), "overview") + " "
+ format(t.inModule(), "module") + " "
+ format(t.inPackage(), "package") + " "
+ format(t.inType(), "type") + " "
+ format(t.inConstructor(),"constructor") + " "
+ format(t.inMethod(), "method") + " "
+ format(t.inField(), "field") + " "
+ format(t.isInlineTag(), "inline")+ " "
+ format((t instanceof SimpleTaglet) && !((SimpleTaglet)t).enabled, "disabled"));
}
}
private String format(boolean b, String s) {
return b ? s : s.replaceAll(".", "."); // replace all with "."
}
}