# HG changeset patch # User aefimov # Date 1468875192 -10800 # Node ID 7d5a50793b6df9ee5b69984888874d5268e95ac9 # Parent cfd609c461fa0287d22e927031bb9dab9599f0f5 8138725: Add options for Javadoc generation Reviewed-by: jjg diff -r cfd609c461fa -r 7d5a50793b6d make/build.properties --- a/make/build.properties Wed Nov 23 00:35:47 2016 -0800 +++ b/make/build.properties Mon Jul 18 23:53:12 2016 +0300 @@ -106,7 +106,8 @@ javadoc.includes = \ com/sun/javadoc/ \ - com/sun/tools/javadoc/ + com/sun/tools/javadoc/ \ + com/sun/tools/doclets/internal/toolkit/util/FatalError.java javadoc.tests = \ tools/javadoc/ diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/doclets/formats/html/ConfigurationImpl.java --- a/src/share/classes/com/sun/tools/doclets/formats/html/ConfigurationImpl.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/doclets/formats/html/ConfigurationImpl.java Mon Jul 18 23:53:12 2016 +0300 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2016, 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 @@ -25,6 +25,8 @@ package com.sun.tools.doclets.formats.html; +import com.sun.tools.javadoc.JavaScriptScanner; +import com.sun.tools.javadoc.RootDocImpl; import com.sun.tools.doclets.internal.toolkit.*; import com.sun.tools.doclets.internal.toolkit.util.*; @@ -164,6 +166,11 @@ public boolean createoverview = false; /** + * Whether or not to check for JavaScript in doc comments. + */ + private boolean allowScriptInComments; + + /** * Unique Resource Handler for this package. */ public final MessageRetriever standardmessage; @@ -259,8 +266,11 @@ nooverview = true; } else if (opt.equals("-overview")) { overview = true; + } else if (opt.equals("--allow-script-in-comments")) { + allowScriptInComments = true; } } + if (root.specifiedClasses().length > 0) { Map map = new HashMap(); PackageDoc pd; @@ -274,6 +284,30 @@ } setCreateOverview(); setTopFile(root); + + if (root instanceof RootDocImpl) { + JavaScriptScanner jss = ((RootDocImpl) root).initJavaScriptScanner(isAllowScriptInComments()); + if (jss != null) { + // In a more object-oriented world, this would be done by methods on the Option objects. + // Note that -windowtitle silently removes any and all HTML elements, and so does not need + // to be handled here. + checkJavaScript(jss, "-header", header); + checkJavaScript(jss, "-footer", footer); + checkJavaScript(jss, "-top", top); + checkJavaScript(jss, "-bottom", bottom); + checkJavaScript(jss, "-doctitle", doctitle); + checkJavaScript(jss, "-packagesheader", packagesheader); + } + } + } + + private void checkJavaScript(JavaScriptScanner jss, final String opt, String value) { + jss.parse(value, new JavaScriptScanner.Reporter() { + public void report() { + root.printError(getText("doclet.JavaScript_in_option", opt)); + throw new FatalError(); + } + }); } /** @@ -307,7 +341,9 @@ option.equals("-serialwarn") || option.equals("-use") || option.equals("-nonavbar") || - option.equals("-nooverview")) { + option.equals("-nooverview") || + option.startsWith("-xdoclint:") || + option.equals("--allow-script-in-comments")) { return 1; } else if (option.equals("-help")) { System.out.println(getText("doclet.usage")); @@ -503,4 +539,13 @@ else return Locale.getDefault(); } + + /** + * Returns whether or not to allow JavaScript in comments. + * Default is off; can be set true from a command line option. + * @return the allowScriptInComments + */ + public boolean isAllowScriptInComments() { + return allowScriptInComments; + } } diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/doclets/formats/html/HtmlDoclet.java --- a/src/share/classes/com/sun/tools/doclets/formats/html/HtmlDoclet.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/doclets/formats/html/HtmlDoclet.java Mon Jul 18 23:53:12 2016 +0300 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2016, 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 @@ -185,6 +185,8 @@ } catch (Exception e) { e.printStackTrace(); throw new DocletAbortException(); + } catch (FatalError fe) { + throw fe; } } } diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/doclets/internal/toolkit/AbstractDoclet.java --- a/src/share/classes/com/sun/tools/doclets/internal/toolkit/AbstractDoclet.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/doclets/internal/toolkit/AbstractDoclet.java Mon Jul 18 23:53:12 2016 +0300 @@ -81,6 +81,8 @@ } try { doclet.startGeneration(root); + } catch (FatalError e) { + return false; } catch (Exception exc) { exc.printStackTrace(); return false; diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/doclets/internal/toolkit/builders/AbstractBuilder.java --- a/src/share/classes/com/sun/tools/doclets/internal/toolkit/builders/AbstractBuilder.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/doclets/internal/toolkit/builders/AbstractBuilder.java Mon Jul 18 23:53:12 2016 +0300 @@ -108,7 +108,14 @@ configuration.root.printError("Unknown element: " + component); throw new DocletAbortException(); } catch (InvocationTargetException e) { - e.getCause().printStackTrace(); + Throwable cause = e.getCause(); + if (cause instanceof FatalError) { + throw (FatalError) cause; + } else if (cause instanceof DocletAbortException) { + throw (DocletAbortException) cause; + } else { + cause.printStackTrace(); + } } catch (Exception e) { e.printStackTrace(); configuration.root.printError("Exception " + diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/doclets/internal/toolkit/resources/doclets.properties --- a/src/share/classes/com/sun/tools/doclets/internal/toolkit/resources/doclets.properties Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/doclets/internal/toolkit/resources/doclets.properties Mon Jul 18 23:53:12 2016 +0300 @@ -29,6 +29,8 @@ doclet.Building_Tree=Building tree for all the packages and classes... doclet.Building_Index=Building index for all the packages and classes... doclet.Building_Index_For_All_Classes=Building index for all classes... +doclet.JavaScript_in_option=Argument for {0} contains JavaScript.\n\ +Use --allow-script-in-comments to allow use of JavaScript. doclet.sourcetab_warning=The argument for -sourcetab must be an integer greater than 0. doclet.Packages=Packages doclet.Other_Packages=Other Packages diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/doclets/internal/toolkit/util/FatalError.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/share/classes/com/sun/tools/doclets/internal/toolkit/util/FatalError.java Mon Jul 18 23:53:12 2016 +0300 @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016, 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 com.sun.tools.doclets.internal.toolkit.util; + +/** + *

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. + */ +@Deprecated +public class FatalError extends Error { + private static final long serialVersionUID = -9131058909576418984L; + + public FatalError() { } +} diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/javac/parser/Parser.java --- a/src/share/classes/com/sun/tools/javac/parser/Parser.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/javac/parser/Parser.java Mon Jul 18 23:53:12 2016 +0300 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2016, 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 @@ -435,7 +435,7 @@ /** * Ident = IDENTIFIER */ - Name ident() { + public Name ident() { if (S.token() == IDENTIFIER) { Name name = S.name(); S.nextToken(); diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/javadoc/DocEnv.java --- a/src/share/classes/com/sun/tools/javadoc/DocEnv.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/javadoc/DocEnv.java Mon Jul 18 23:53:12 2016 +0300 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2016, 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 @@ -76,7 +76,7 @@ JavadocEnter enter; /** The name table. */ - Name.Table names; + private final Name.Table names; /** The encoding name. */ private String encoding; @@ -97,6 +97,7 @@ Check chk; Types types; JavaFileManager fileManager; + JavaScriptScanner javaScriptScanner; /** Allow documenting from class files? */ boolean docClasses = false; @@ -765,4 +766,13 @@ result |= Modifier.VOLATILE; return result; } + + JavaScriptScanner initJavaScriptScanner(boolean allowScriptInComments) { + if (allowScriptInComments) { + javaScriptScanner = null; + } else { + javaScriptScanner = new JavaScriptScanner(); + } + return javaScriptScanner; + } } diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/javadoc/DocImpl.java --- a/src/share/classes/com/sun/tools/javadoc/DocImpl.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/javadoc/DocImpl.java Mon Jul 18 23:53:12 2016 +0300 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2016, 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 @@ -32,6 +32,7 @@ import com.sun.javadoc.*; +import com.sun.tools.doclets.internal.toolkit.util.FatalError; import com.sun.tools.javac.util.Position; /** @@ -99,7 +100,17 @@ */ Comment comment() { if (comment == null) { - comment = new Comment(this, documentation()); + String d = documentation(); + if (env.javaScriptScanner != null) { + env.javaScriptScanner.parse(d, new JavaScriptScanner.Reporter() { + @Override + public void report() { + env.error(DocImpl.this, "javadoc.JavaScript_in_comment"); + throw new FatalError(); + } + }); + } + comment = new Comment(this, d); } return comment; } diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/javadoc/JavaScriptScanner.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/share/classes/com/sun/tools/javadoc/JavaScriptScanner.java Mon Jul 18 23:53:12 2016 +0300 @@ -0,0 +1,1103 @@ +/* + * Copyright (c) 2016, 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 com.sun.tools.javadoc; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import com.sun.tools.javadoc.JavaScriptScanner.TagParser.Kind; + +import static com.sun.tools.javac.util.LayoutCharacters.EOI; + +/** + * Parser to detect use of JavaScript in documentation comments. + */ +@Deprecated +public class JavaScriptScanner { + public static interface Reporter { + void report(); + } + + static class ParseException extends Exception { + private static final long serialVersionUID = 0; + ParseException(String key) { + super(key); + } + } + + private Reporter reporter; + + /** The input buffer, index of most recent character read, + * index of one past last character in buffer. + */ + protected char[] buf; + protected int bp; + protected int buflen; + + /** The current character. + */ + protected char ch; + + private boolean newline = true; + + Map tagParsers; + Set eventAttrs; + Set uriAttrs; + + public JavaScriptScanner() { + initTagParsers(); + initEventAttrs(); + initURIAttrs(); + } + + public void parse(String comment, Reporter r) { + reporter = r; + String c = comment; + buf = new char[c.length() + 1]; + c.getChars(0, c.length(), buf, 0); + buf[buf.length - 1] = EOI; + buflen = buf.length - 1; + bp = -1; + newline = true; + nextChar(); + + blockContent(); + blockTags(); + } + + private void checkHtmlTag(String tag) { + if (tag.equalsIgnoreCase("script")) { + reporter.report(); + } + } + + private void checkHtmlAttr(String name, String value) { + String n = name.toLowerCase(Locale.ENGLISH); + if (eventAttrs.contains(n) + || uriAttrs.contains(n) + && value != null && value.toLowerCase(Locale.ENGLISH).trim().startsWith("javascript:")) { + reporter.report(); + } + } + + void nextChar() { + ch = buf[bp < buflen ? ++bp : buflen]; + switch (ch) { + case '\f': case '\n': case '\r': + newline = true; + } + } + + /** + * Read block content, consisting of text, html and inline tags. + * Terminated by the end of input, or the beginning of the next block tag: + * i.e. @ as the first non-whitespace character on a line. + */ + @SuppressWarnings("fallthrough") + protected void blockContent() { + + loop: + while (bp < buflen) { + switch (ch) { + case '\n': case '\r': case '\f': + newline = true; + // fallthrough + + case ' ': case '\t': + nextChar(); + break; + + case '&': + entity(null); + break; + + case '<': + html(); + break; + + case '>': + newline = false; + nextChar(); + break; + + case '{': + inlineTag(null); + break; + + case '@': + if (newline) { + break loop; + } + // fallthrough + + default: + newline = false; + nextChar(); + } + } + } + + /** + * Read a series of block tags, including their content. + * Standard tags parse their content appropriately. + * Non-standard tags are represented by {@link UnknownBlockTag}. + */ + protected void blockTags() { + while (ch == '@') + blockTag(); + } + + /** + * Read a single block tag, including its content. + * Standard tags parse their content appropriately. + * Non-standard tags are represented by {@link UnknownBlockTag}. + */ + protected void blockTag() { + int p = bp; + try { + nextChar(); + if (isIdentifierStart(ch)) { + String name = readTagName(); + TagParser tp = tagParsers.get(name); + if (tp == null) { + blockContent(); + } else { + switch (tp.getKind()) { + case BLOCK: + tp.parse(p); + return; + case INLINE: + return; + } + } + } + blockContent(); + } catch (ParseException e) { + blockContent(); + } + } + + protected void inlineTag(Void list) { + newline = false; + nextChar(); + if (ch == '@') { + inlineTag(); + } + } + + /** + * Read a single inline tag, including its content. + * Standard tags parse their content appropriately. + * Non-standard tags are represented by {@link UnknownBlockTag}. + * Malformed tags may be returned as {@link Erroneous}. + */ + protected void inlineTag() { + int p = bp - 1; + try { + nextChar(); + if (isIdentifierStart(ch)) { + String name = readTagName(); + TagParser tp = tagParsers.get(name); + + if (tp == null) { + skipWhitespace(); + inlineText(WhitespaceRetentionPolicy.REMOVE_ALL); + nextChar(); + } else { + skipWhitespace(); + if (tp.getKind() == TagParser.Kind.INLINE) { + tp.parse(p); + } else { // handle block tags (ex: @see) in inline content + inlineText(WhitespaceRetentionPolicy.REMOVE_ALL); // skip content + nextChar(); + } + } + } + } catch (ParseException e) { + } + } + + private static enum WhitespaceRetentionPolicy { + RETAIN_ALL, + REMOVE_FIRST_SPACE, + REMOVE_ALL + } + + /** + * Read plain text content of an inline tag. + * Matching pairs of { } are skipped; the text is terminated by the first + * unmatched }. It is an error if the beginning of the next tag is detected. + */ + private void inlineText(WhitespaceRetentionPolicy whitespacePolicy) throws ParseException { + switch (whitespacePolicy) { + case REMOVE_ALL: + skipWhitespace(); + break; + case REMOVE_FIRST_SPACE: + if (ch == ' ') + nextChar(); + break; + case RETAIN_ALL: + default: + // do nothing + break; + + } + int pos = bp; + int depth = 1; + + loop: + while (bp < buflen) { + switch (ch) { + case '\n': case '\r': case '\f': + newline = true; + break; + + case ' ': case '\t': + break; + + case '{': + newline = false; + depth++; + break; + + case '}': + if (--depth == 0) { + return; + } + newline = false; + break; + + case '@': + if (newline) + break loop; + newline = false; + break; + + default: + newline = false; + break; + } + nextChar(); + } + throw new ParseException("dc.unterminated.inline.tag"); + } + + /** + * Read Java class name, possibly followed by member + * Matching pairs of {@literal < >} are skipped. The text is terminated by the first + * unmatched }. It is an error if the beginning of the next tag is detected. + */ + // TODO: boolean allowMember should be enum FORBID, ALLOW, REQUIRE + // TODO: improve quality of parse to forbid bad constructions. + // TODO: update to use ReferenceParser + @SuppressWarnings("fallthrough") + protected void reference(boolean allowMember) throws ParseException { + int pos = bp; + int depth = 0; + + // scan to find the end of the signature, by looking for the first + // whitespace not enclosed in () or <>, or the end of the tag + loop: + while (bp < buflen) { + switch (ch) { + case '\n': case '\r': case '\f': + newline = true; + // fallthrough + + case ' ': case '\t': + if (depth == 0) + break loop; + break; + + case '(': + case '<': + newline = false; + depth++; + break; + + case ')': + case '>': + newline = false; + --depth; + break; + + case '}': + if (bp == pos) + return; + newline = false; + break loop; + + case '@': + if (newline) + break loop; + // fallthrough + + default: + newline = false; + + } + nextChar(); + } + + if (depth != 0) + throw new ParseException("dc.unterminated.signature"); + } + + /** + * Read Java identifier + * Matching pairs of { } are skipped; the text is terminated by the first + * unmatched }. It is an error if the beginning of the next tag is detected. + */ + @SuppressWarnings("fallthrough") + protected void identifier() throws ParseException { + skipWhitespace(); + int pos = bp; + + if (isJavaIdentifierStart(ch)) { + readJavaIdentifier(); + return; + } + + throw new ParseException("dc.identifier.expected"); + } + + /** + * Read a quoted string. + * It is an error if the beginning of the next tag is detected. + */ + @SuppressWarnings("fallthrough") + protected void quotedString() { + int pos = bp; + nextChar(); + + loop: + while (bp < buflen) { + switch (ch) { + case '\n': case '\r': case '\f': + newline = true; + break; + + case ' ': case '\t': + break; + + case '"': + nextChar(); + // trim trailing white-space? + return; + + case '@': + if (newline) + break loop; + + } + nextChar(); + } + } + + /** + * Read a term ie. one word. + * It is an error if the beginning of the next tag is detected. + */ + @SuppressWarnings("fallthrough") + protected void inlineWord() { + int pos = bp; + int depth = 0; + loop: + while (bp < buflen) { + switch (ch) { + case '\n': + newline = true; + // fallthrough + + case '\r': case '\f': case ' ': case '\t': + return; + + case '@': + if (newline) + break loop; + + case '{': + depth++; + break; + + case '}': + if (depth == 0 || --depth == 0) + return; + break; + } + newline = false; + nextChar(); + } + } + + /** + * Read general text content of an inline tag, including HTML entities and elements. + * Matching pairs of { } are skipped; the text is terminated by the first + * unmatched }. It is an error if the beginning of the next tag is detected. + */ + @SuppressWarnings("fallthrough") + private void inlineContent() { + + skipWhitespace(); + int pos = bp; + int depth = 1; + + loop: + while (bp < buflen) { + + switch (ch) { + case '\n': case '\r': case '\f': + newline = true; + // fall through + + case ' ': case '\t': + nextChar(); + break; + + case '&': + entity(null); + break; + + case '<': + newline = false; + html(); + break; + + case '{': + newline = false; + depth++; + nextChar(); + break; + + case '}': + newline = false; + if (--depth == 0) { + nextChar(); + return; + } + nextChar(); + break; + + case '@': + if (newline) + break loop; + // fallthrough + + default: + nextChar(); + break; + } + } + + } + + protected void entity(Void list) { + newline = false; + entity(); + } + + /** + * Read an HTML entity. + * {@literal &identifier; } or {@literal &#digits; } or {@literal &#xhex-digits; } + */ + protected void entity() { + nextChar(); + String name = null; + if (ch == '#') { + int namep = bp; + nextChar(); + if (isDecimalDigit(ch)) { + nextChar(); + while (isDecimalDigit(ch)) + nextChar(); + name = new String(buf, namep, bp - namep); + } else if (ch == 'x' || ch == 'X') { + nextChar(); + if (isHexDigit(ch)) { + nextChar(); + while (isHexDigit(ch)) + nextChar(); + name = new String(buf, namep, bp - namep); + } + } + } else if (isIdentifierStart(ch)) { + name = readIdentifier(); + } + + if (name != null) { + if (ch != ';') + return; + nextChar(); + } + } + + /** + * Read the start or end of an HTML tag, or an HTML comment + * {@literal } or {@literal } + */ + protected void html() { + int p = bp; + nextChar(); + if (isIdentifierStart(ch)) { + String name = readIdentifier(); + checkHtmlTag(name); + htmlAttrs(); + if (ch == '/') { + nextChar(); + } + if (ch == '>') { + nextChar(); + return; + } + } else if (ch == '/') { + nextChar(); + if (isIdentifierStart(ch)) { + readIdentifier(); + skipWhitespace(); + if (ch == '>') { + nextChar(); + return; + } + } + } else if (ch == '!') { + nextChar(); + if (ch == '-') { + nextChar(); + if (ch == '-') { + nextChar(); + while (bp < buflen) { + int dash = 0; + while (ch == '-') { + dash++; + nextChar(); + } + // Strictly speaking, a comment should not contain "--" + // so dash > 2 is an error, dash == 2 implies ch == '>' + // See http://www.w3.org/TR/html-markup/syntax.html#syntax-comments + // for more details. + if (dash >= 2 && ch == '>') { + nextChar(); + return; + } + + nextChar(); + } + } + } + } + + bp = p + 1; + ch = buf[bp]; + } + + /** + * Read a series of HTML attributes, terminated by {@literal > }. + * Each attribute is of the form {@literal identifier[=value] }. + * "value" may be unquoted, single-quoted, or double-quoted. + */ + protected void htmlAttrs() { + skipWhitespace(); + + loop: + while (isIdentifierStart(ch)) { + int namePos = bp; + String name = readAttributeName(); + skipWhitespace(); + StringBuilder value = new StringBuilder(); + if (ch == '=') { + nextChar(); + skipWhitespace(); + if (ch == '\'' || ch == '"') { + char quote = ch; + nextChar(); + while (bp < buflen && ch != quote) { + if (newline && ch == '@') { + // No point trying to read more. + // In fact, all attrs get discarded by the caller + // and superseded by a malformed.html node because + // the html tag itself is not terminated correctly. + break loop; + } + value.append(ch); + nextChar(); + } + nextChar(); + } else { + while (bp < buflen && !isUnquotedAttrValueTerminator(ch)) { + value.append(ch); + nextChar(); + } + } + skipWhitespace(); + } + checkHtmlAttr(name, value.toString()); + } + } + + protected void attrValueChar(Void list) { + switch (ch) { + case '&': + entity(list); + break; + + case '{': + inlineTag(list); + break; + + default: + nextChar(); + } + } + + protected boolean isIdentifierStart(char ch) { + return Character.isUnicodeIdentifierStart(ch); + } + + protected String readIdentifier() { + int start = bp; + nextChar(); + while (bp < buflen && Character.isUnicodeIdentifierPart(ch)) + nextChar(); + return new String(buf, start, bp - start); + } + + protected String readAttributeName() { + int start = bp; + nextChar(); + while (bp < buflen && (Character.isUnicodeIdentifierPart(ch) || ch == '-')) + nextChar(); + return new String(buf, start, bp - start); + } + + protected String readTagName() { + int start = bp; + nextChar(); + while (bp < buflen + && (Character.isUnicodeIdentifierPart(ch) || ch == '.' + || ch == '-' || ch == ':')) { + nextChar(); + } + return new String(buf, start, bp - start); + } + + protected boolean isJavaIdentifierStart(char ch) { + return Character.isJavaIdentifierStart(ch); + } + + protected String readJavaIdentifier() { + int start = bp; + nextChar(); + while (bp < buflen && Character.isJavaIdentifierPart(ch)) + nextChar(); + return new String(buf, start, bp - start); + } + + protected boolean isDecimalDigit(char ch) { + return ('0' <= ch && ch <= '9'); + } + + protected boolean isHexDigit(char ch) { + return ('0' <= ch && ch <= '9') + || ('a' <= ch && ch <= 'f') + || ('A' <= ch && ch <= 'F'); + } + + protected boolean isUnquotedAttrValueTerminator(char ch) { + switch (ch) { + case '\f': case '\n': case '\r': case '\t': + case ' ': + case '"': case '\'': case '`': + case '=': case '<': case '>': + return true; + default: + return false; + } + } + + protected boolean isWhitespace(char ch) { + return Character.isWhitespace(ch); + } + + protected void skipWhitespace() { + while (isWhitespace(ch)) { + nextChar(); + } + } + + /** + * @param start position of first character of string + * @param end position of character beyond last character to be included + */ + String newString(int start, int end) { + return new String(buf, start, end - start); + } + + static abstract class TagParser { + enum Kind { INLINE, BLOCK } + + final Kind kind; + final String name; + + + TagParser(Kind k, String tk) { + kind = k; + name = tk; + } + + TagParser(Kind k, String tk, boolean retainWhiteSpace) { + this(k, tk); + } + + Kind getKind() { + return kind; + } + + String getName() { + return name; + } + + abstract void parse(int pos) throws ParseException; + } + + /** + * @see Javadoc Tags + */ + @SuppressWarnings("deprecation") + private void initTagParsers() { + TagParser[] parsers = { + // @author name-text + new TagParser(Kind.BLOCK, "author") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + + // {@code text} + new TagParser(Kind.INLINE, "code", true) { + @Override + public void parse(int pos) throws ParseException { + inlineText(WhitespaceRetentionPolicy.REMOVE_FIRST_SPACE); + nextChar(); + } + }, + + // @deprecated deprecated-text + new TagParser(Kind.BLOCK, "deprecated") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + + // {@docRoot} + new TagParser(Kind.INLINE, "docRoot") { + @Override + public void parse(int pos) throws ParseException { + if (ch == '}') { + nextChar(); + return; + } + inlineText(WhitespaceRetentionPolicy.REMOVE_ALL); // skip unexpected content + nextChar(); + throw new ParseException("dc.unexpected.content"); + } + }, + + // @exception class-name description + new TagParser(Kind.BLOCK, "exception") { + @Override + public void parse(int pos) throws ParseException { + skipWhitespace(); + reference(false); + blockContent(); + } + }, + + // @hidden hidden-text + new TagParser(Kind.BLOCK, "hidden") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + + // @index search-term options-description + new TagParser(Kind.INLINE, "index") { + @Override + public void parse(int pos) throws ParseException { + skipWhitespace(); + if (ch == '}') { + throw new ParseException("dc.no.content"); + } + if (ch == '"') quotedString(); else inlineWord(); + skipWhitespace(); + if (ch != '}') { + inlineContent(); + } else { + nextChar(); + } + } + }, + + // {@inheritDoc} + new TagParser(Kind.INLINE, "inheritDoc") { + @Override + public void parse(int pos) throws ParseException { + if (ch == '}') { + nextChar(); + return; + } + inlineText(WhitespaceRetentionPolicy.REMOVE_ALL); // skip unexpected content + nextChar(); + throw new ParseException("dc.unexpected.content"); + } + }, + + // {@link package.class#member label} + new TagParser(Kind.INLINE, "link") { + @Override + public void parse(int pos) throws ParseException { + reference(true); + inlineContent(); + } + }, + + // {@linkplain package.class#member label} + new TagParser(Kind.INLINE, "linkplain") { + @Override + public void parse(int pos) throws ParseException { + reference(true); + inlineContent(); + } + }, + + // {@literal text} + new TagParser(Kind.INLINE, "literal", true) { + @Override + public void parse(int pos) throws ParseException { + inlineText(WhitespaceRetentionPolicy.REMOVE_FIRST_SPACE); + nextChar(); + } + }, + + // @param parameter-name description + new TagParser(Kind.BLOCK, "param") { + @Override + public void parse(int pos) throws ParseException { + skipWhitespace(); + + boolean typaram = false; + if (ch == '<') { + typaram = true; + nextChar(); + } + + identifier(); + + if (typaram) { + if (ch != '>') + throw new ParseException("dc.gt.expected"); + nextChar(); + } + + skipWhitespace(); + blockContent(); + } + }, + + // @return description + new TagParser(Kind.BLOCK, "return") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + + // @see reference | quoted-string | HTML + new TagParser(Kind.BLOCK, "see") { + @Override + public void parse(int pos) throws ParseException { + skipWhitespace(); + switch (ch) { + case '"': + quotedString(); + skipWhitespace(); + if (ch == '@' + || ch == EOI && bp == buf.length - 1) { + return; + } + break; + + case '<': + blockContent(); + return; + + case '@': + if (newline) + throw new ParseException("dc.no.content"); + break; + + case EOI: + if (bp == buf.length - 1) + throw new ParseException("dc.no.content"); + break; + + default: + if (isJavaIdentifierStart(ch) || ch == '#') { + reference(true); + blockContent(); + } + } + throw new ParseException("dc.unexpected.content"); + } + }, + + // @serialData data-description + new TagParser(Kind.BLOCK, "@serialData") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + + // @serialField field-name field-type description + new TagParser(Kind.BLOCK, "serialField") { + @Override + public void parse(int pos) throws ParseException { + skipWhitespace(); + identifier(); + skipWhitespace(); + reference(false); + if (isWhitespace(ch)) { + skipWhitespace(); + blockContent(); + } + } + }, + + // @serial field-description | include | exclude + new TagParser(Kind.BLOCK, "serial") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + + // @since since-text + new TagParser(Kind.BLOCK, "since") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + + // @throws class-name description + new TagParser(Kind.BLOCK, "throws") { + @Override + public void parse(int pos) throws ParseException { + skipWhitespace(); + reference(false); + blockContent(); + } + }, + + // {@value package.class#field} + new TagParser(Kind.INLINE, "value") { + @Override + public void parse(int pos) throws ParseException { + reference(true); + skipWhitespace(); + if (ch == '}') { + nextChar(); + return; + } + nextChar(); + throw new ParseException("dc.unexpected.content"); + } + }, + + // @version version-text + new TagParser(Kind.BLOCK, "version") { + @Override + public void parse(int pos) { + blockContent(); + } + }, + }; + + tagParsers = new HashMap(); + for (TagParser p: parsers) + tagParsers.put(p.getName(), p); + + } + + private void initEventAttrs() { + eventAttrs = new HashSet(Arrays.asList( + // See https://www.w3.org/TR/html-markup/global-attributes.html#common.attrs.event-handler + "onabort", "onblur", "oncanplay", "oncanplaythrough", + "onchange", "onclick", "oncontextmenu", "ondblclick", + "ondrag", "ondragend", "ondragenter", "ondragleave", + "ondragover", "ondragstart", "ondrop", "ondurationchange", + "onemptied", "onended", "onerror", "onfocus", "oninput", + "oninvalid", "onkeydown", "onkeypress", "onkeyup", + "onload", "onloadeddata", "onloadedmetadata", "onloadstart", + "onmousedown", "onmousemove", "onmouseout", "onmouseover", + "onmouseup", "onmousewheel", "onpause", "onplay", + "onplaying", "onprogress", "onratechange", "onreadystatechange", + "onreset", "onscroll", "onseeked", "onseeking", + "onselect", "onshow", "onstalled", "onsubmit", "onsuspend", + "ontimeupdate", "onvolumechange", "onwaiting", + + // See https://www.w3.org/TR/html4/sgml/dtd.html + // Most of the attributes that take a %Script are also defined as event handlers + // in HTML 5. The one exception is onunload. + // "onchange", "onclick", "ondblclick", "onfocus", + // "onkeydown", "onkeypress", "onkeyup", "onload", + // "onmousedown", "onmousemove", "onmouseout", "onmouseover", + // "onmouseup", "onreset", "onselect", "onsubmit", + "onunload" + )); + } + + private void initURIAttrs() { + uriAttrs = new HashSet(Arrays.asList( + // See https://www.w3.org/TR/html4/sgml/dtd.html + // https://www.w3.org/TR/html5/ + // These are all the attributes that take a %URI or a valid URL potentially surrounded + // by spaces + "action", "cite", "classid", "codebase", "data", + "datasrc", "for", "href", "longdesc", "profile", + "src", "usemap" + )); + } + +} diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/javadoc/RootDocImpl.java --- a/src/share/classes/com/sun/tools/javadoc/RootDocImpl.java Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/javadoc/RootDocImpl.java Mon Jul 18 23:53:12 2016 +0300 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2016, 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 @@ -361,4 +361,8 @@ public Locale getLocale() { return env.doclocale.locale; } + + public JavaScriptScanner initJavaScriptScanner(boolean allowScriptInComments) { + return env.initJavaScriptScanner(allowScriptInComments); + } } diff -r cfd609c461fa -r 7d5a50793b6d src/share/classes/com/sun/tools/javadoc/resources/javadoc.properties --- a/src/share/classes/com/sun/tools/javadoc/resources/javadoc.properties Wed Nov 23 00:35:47 2016 -0800 +++ b/src/share/classes/com/sun/tools/javadoc/resources/javadoc.properties Mon Jul 18 23:53:12 2016 +0300 @@ -1,5 +1,5 @@ # -# Copyright (c) 1997, 2004, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 1997, 2016, 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 @@ -101,6 +101,8 @@ javadoc.Body_missing_from_html_file=Body tag missing from HTML javadoc.End_body_missing_from_html_file=Close body tag missing from HTML file javadoc.Multiple_package_comments=Multiple sources of package comments found for package "{0}" +javadoc.JavaScript_in_comment=JavaScript found in documentation comment.\n\ + Use --allow-script-in-comments to allow use of JavaScript. javadoc.class_not_found=Class {0} not found. javadoc.error=error javadoc.warning=warning diff -r cfd609c461fa -r 7d5a50793b6d test/tools/javadoc/TestScriptInComment.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/tools/javadoc/TestScriptInComment.java Mon Jul 18 23:53:12 2016 +0300 @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2016, 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. + */ + +/** + * @test + * @bug 8138725 + * @summary test --allow-script-in-comments + * @run main TestScriptInComment + */ + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Combo-style test, exercising combinations of different HTML fragments that may contain + * JavaScript, different places to place those fragments, and whether or not to allow the use + * of JavaScript. + */ +public class TestScriptInComment { + public static void main(String... args) throws Exception { + new TestScriptInComment().run(); + } + + /** + * Representative samples of different fragments of HTML that may contain JavaScript. + * To facilitate checking the output manually in a browser, the text "#ALERT" will be + * replaced by a JavaScript call of "alert(msg)", using a message string that is specific + * to the test case. + */ + enum Comment { + LC("", true), // script tag in Lower Case + UC("", true), // script tag in Upper Case + WS("< script >#ALERT", false, "-Xdoclint:none"), // script tag with invalid white space + SA("", true), // script tag with an attribute + ON("x", true), // event handler attribute + URI("x", true); // javadcript URI + + /** + * Creates an HTML fragment to be injected into a template. + * @param text the HTML fragment to put into a doc comment or option. + * @param hasScript whether or not this fragment does contain legal JavaScript + * @param opts any additional options to be specified when javadoc is run + */ + Comment(String text, boolean hasScript, String... opts) { + this.text = text; + this.hasScript = hasScript; + this.opts = Arrays.asList(opts); + } + + final String text; + final boolean hasScript; + final List opts; + }; + + /** + * Representative samples of positions in which javadoc may find JavaScript. + * Each template contains a series of strings, which are written to files or inferred as options. + * The first source file implies a corresponding output file which should not be written + * if the comment contains JavaScript and JavaScript is not allowed. + */ + enum Template { + OVR(" overview #COMMENT ", "package p; public class C { }"), + PKGINFO("#COMMENT package p;", "package p; public class C { }"), + PKGHTML("#COMMENT package p;", "package p; public class C { }"), + CLS("package p; #COMMENT public class C { }"), + CON("package p; public class C { #COMMENT public C() { } }"), + FLD("package p; public class C { #COMMENT public int f; }"), + MTH("package p; public class C { #COMMENT public void m() { } }"), + TOP("-top", "lorem #COMMENT ipsum", "package p; public class C { }"), + HDR("-header", "lorem #COMMENT ipsum", "package p; public class C { }"), + FTR("-footer", "lorem #COMMENT ipsum", "package p; public class C { }"), + BTM("-bottom", "lorem #COMMENT ipsum", "package p; public class C { }"), + DTTL("-doctitle", "lorem #COMMENT ipsum", "package p; public class C { }"), + PHDR("-packagesheader", "lorem #COMMENT ipsum", "package p; public class C { }"); + + Template(String... args) { + opts = new ArrayList(); + sources = new ArrayList(); + int i = 0; + while (args[i].startsWith("-")) { + // all options being tested have a single argument that follow the option + opts.add(args[i++]); + opts.add(args[i++]); + } + while(i < args.length) { + sources.add(args[i++]); + } + } + + // groups: 1 or not; 2: package name; 3: class name + private final Pattern pat = + Pattern.compile("(?i)()?.*?(?:package ([a-z]+);.*?(?:class ([a-z]+).*)?)?"); + + /** + * Infer the file in which to write the given source. + * @param dir the base source directory + * @param src the source text + * @return the file in which the source should be written + */ + File getSrcFile(File srcDir, String src) { + String f; + Matcher m = pat.matcher(src); + if (!m.matches()) + throw new Error("match failed"); + if (m.group(3) != null) { + f = m.group(2) + "/" + m.group(3) + ".java"; + } else if (m.group(2) != null) { + f = m.group(2) + "/" + (m.group(1) == null ? "package-info.java" : "package.html"); + } else { + f = "overview.html"; + } + return new File(srcDir, f); + } + + /** + * Get the options to give to javadoc. + * @param srcDir the srcDir to use -overview is needed + * @return + */ + List getOpts(File srcDir) { + if (!opts.isEmpty()) { + return opts; + } else if (sources.get(0).contains("overview")) { + return Arrays.asList("-overview", getSrcFile(srcDir, sources.get(0)).getPath()); + } else { + return Collections.emptyList(); + } + } + + /** + * Gets the output file corresponding to the first source file. + * This file should not be written if the comment contains JavaScript and JavaScripot is + * not allowed. + * @param dir the base output directory + * @return the output file + */ + File getOutFile(File outDir) { + String f; + Matcher m = pat.matcher(sources.get(0)); + if (!m.matches()) + throw new Error("match failed"); + if (m.group(3) != null) { + f = m.group(2) + "/" + m.group(3) + ".html"; + } else if (m.group(2) != null) { + f = m.group(2) + "/package-summary.html"; + } else { + f = "overview-summary.html"; + } + return new File(outDir, f); + } + + final List opts; + final List sources; + }; + + enum Option { + OFF(null), + ON("--allow-script-in-comments"); + + Option(String text) { + this.text = text; + } + + final String text; + }; + + private PrintStream out = System.err; + + public void run() throws Exception { + int count = 0; + for (Template template: Template.values()) { + for (Comment comment: Comment.values()) { + for (Option option: Option.values()) { + if (test(template, comment, option)) { + count++; + } + } + } + } + + out.println(count + " test cases run"); + if (errors > 0) { + throw new Exception(errors + " errors occurred"); + } + } + + boolean test(Template template, Comment comment, Option option) throws IOException { + if (option == Option.ON && !comment.hasScript) { + // skip --allowScriptInComments if comment does not contain JavaScript + return false; + } + + String test = template + "-" + comment + "-" + option; + out.println("Test: " + test); + + File dir = new File(test); + dir.mkdirs(); + File srcDir = new File(dir, "src"); + File outDir = new File(dir, "out"); + + String alert = "alert(\"" + test + "\");"; + for (String src: template.sources) { + writeFile(template.getSrcFile(srcDir, src), + src.replace("#COMMENT", + "/** " + comment.text.replace("#ALERT", alert) + " **/")); + } + + List opts = new ArrayList(); + opts.add("-sourcepath"); + opts.add(srcDir.getPath()); + opts.add("-d"); + opts.add(outDir.getPath()); + if (option.text != null) + opts.add(option.text); + for (String opt: template.getOpts(srcDir)) { + opts.add(opt.replace("#COMMENT", comment.text.replace("#ALERT", alert))); + } + opts.addAll(comment.opts); + opts.add("-noindex"); // index not required; save time/space writing files + opts.add("p"); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + int rc = javadoc(opts, pw); + pw.close(); + String log = sw.toString(); + writeFile(new File(dir, "log.txt"), log); + + out.println("opts: " + opts); + out.println(" rc: " + rc); + out.println(" log:"); + out.println(log); + + String ERROR = "Use --allow-script-in-comment"; + File outFile = template.getOutFile(outDir); + + boolean expectErrors = comment.hasScript && (option == Option.OFF); + + if (expectErrors) { + check(rc != 0, "unexpected exit code: " + rc); + check(log.contains(ERROR), "expected error message not found"); + check(!existsAndNotEmpty(outFile), "output file found unexpectedly"); + } else { + check(rc == 0, "unexpected exit code: " + rc); + check(!log.contains(ERROR), "error message found"); + check(outFile.exists(), "output file not found"); + } + + out.println(); + return true; + } + + int javadoc(List opts, PrintWriter pw) { + return com.sun.tools.javadoc.Main.execute("javadoc", pw, pw, pw, + "com.sun.tools.doclets.standard.Standard", opts.toArray(new String[opts.size()])); + } + + File writeFile(File f, String text) throws IOException { + f.getParentFile().mkdirs(); + FileWriter fw = new FileWriter(f); + try { + fw.write(text); + } finally { + fw.close(); + } + return f; + } + + void check(boolean cond, String errMessage) { + if (!cond) { + error(errMessage); + } + } + + void error(String message) { + out.println("Error: " + message); + errors++; + } + + int errors = 0; + + boolean existsAndNotEmpty(File f) throws IOException { + if (!f.exists()) { + return false; + } + RandomAccessFile raf = new RandomAccessFile(f, "r"); + return (raf.length() > 0); + } +} +