blob: acc3a655e30d2c37b7b60a48e5556a6a104391ad [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.tools.ant.taskdefs;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.stream.Collectors;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.MagicNames;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.DirSet;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileProvider;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.JavaEnvUtils;
/**
* Generates Javadoc documentation for a collection
* of source code.
*
* <p>Current known limitations are:</p>
*
* <ul>
* <li>patterns must be of the form "xxx.*", every other pattern doesn't
* work.
* <li>there is no control on arguments sanity since they are left
* to the Javadoc implementation.
* </ul>
*
* <p>If no <code>doclet</code> is set, then the <code>version</code> and
* <code>author</code> are by default <code>"yes"</code>.</p>
*
* <p>Note: This task is run on another VM because the Javadoc code calls
* <code>System.exit()</code> which would break Ant functionality.</p>
*
* @since Ant 1.1
*
* @ant.task category="java"
*/
public class Javadoc extends Task {
private static final String LOAD_FRAME = "function loadFrames() {";
private static final int LOAD_FRAME_LEN = LOAD_FRAME.length();
/**
* Inner class used to manage doclet parameters.
*/
public class DocletParam {
/** The parameter name */
private String name;
/** The parameter value */
private String value;
/**
* Set the name of the parameter.
*
* @param name the name of the doclet parameter
*/
public void setName(final String name) {
this.name = name;
}
/**
* Get the parameter name.
*
* @return the parameter's name.
*/
public String getName() {
return name;
}
/**
* Set the parameter value.
*
* Note that only string values are supported. No resolution of file
* paths is performed.
*
* @param value the parameter value.
*/
public void setValue(final String value) {
this.value = value;
}
/**
* Get the parameter value.
*
* @return the parameter value.
*/
public String getValue() {
return value;
}
}
/**
* A project aware class used for Javadoc extensions which take a name
* and a path such as doclet and taglet arguments.
*
*/
public static class ExtensionInfo extends ProjectComponent {
/** The name of the extension */
private String name;
/** The optional path to use to load the extension */
private Path path;
/**
* Set the name of the extension
*
* @param name the extension's name.
*/
public void setName(final String name) {
this.name = name;
}
/**
* Get the name of the extension.
*
* @return the extension's name.
*/
public String getName() {
return name;
}
/**
* Set the path to use when loading the component.
*
* @param path a Path instance containing the classpath to use.
*/
public void setPath(final Path path) {
if (this.path == null) {
this.path = path;
} else {
this.path.append(path);
}
}
/**
* Get the extension's path.
*
* @return the path to be used to load the extension.
* May be <code>null</code>
*/
public Path getPath() {
return path;
}
/**
* Create an empty nested path to be configured by Ant with the
* classpath for the extension.
*
* @return a new Path instance to be configured.
*/
public Path createPath() {
if (path == null) {
path = new Path(getProject());
}
return path.createPath();
}
/**
* Adds a reference to a CLASSPATH defined elsewhere.
*
* @param r the reference containing the path.
*/
public void setPathRef(final Reference r) {
createPath().setRefid(r);
}
}
/**
* This class stores info about doclets.
*
*/
public class DocletInfo extends ExtensionInfo {
/** Collection of doclet parameters. */
private final List<DocletParam> params = new Vector<>();
/**
* Create a doclet parameter to be configured by Ant.
*
* @return a new DocletParam instance to be configured.
*/
public DocletParam createParam() {
final DocletParam param = new DocletParam();
params.add(param);
return param;
}
/**
* Get the doclet's parameters.
*
* @return an Enumeration of DocletParam instances.
*/
public Enumeration<DocletParam> getParams() {
return Collections.enumeration(params);
}
}
/**
* Used to track info about the packages to be javadoc'd
*/
public static class PackageName {
/** The package name */
private String name;
/**
* Set the name of the package
*
* @param name the package name.
*/
public void setName(final String name) {
this.name = name.trim();
}
/**
* Get the package name.
*
* @return the package's name.
*/
public String getName() {
return name;
}
/**
* Return a string rep for this object.
* @return the package name.
*/
@Override
public String toString() {
return getName();
}
}
/**
* This class is used to manage the source files to be processed.
*/
public static class SourceFile {
/** The source file */
private File file;
/**
* Default constructor
*/
public SourceFile() {
//empty
}
/**
* Constructor specifying the source file directly
*
* @param file the source file
*/
public SourceFile(final File file) {
this.file = file;
}
/**
* Set the source file.
*
* @param file the source file.
*/
public void setFile(final File file) {
this.file = file;
}
/**
* Get the source file.
*
* @return the source file.
*/
public File getFile() {
return file;
}
}
/**
* An HTML element in the Javadoc.
*
* This class is used for those Javadoc elements which contain HTML such as
* footers, headers, etc.
*/
public static class Html {
/** The text for the element */
private final StringBuffer text = new StringBuffer();
/**
* Add text to the element.
*
* @param t the text to be added.
*/
public void addText(final String t) {
text.append(t);
}
/**
* Get the current text for the element.
*
* @return the current text.
*/
public String getText() {
return text.substring(0);
}
}
/**
* EnumeratedAttribute implementation supporting the Javadoc scoping
* values.
*/
public static class AccessType extends EnumeratedAttribute {
/**
* @return the allowed values for the access type.
*/
@Override
public String[] getValues() {
// Protected first so if any GUI tool offers a default
// based on enum #0, it will be right.
return new String[] {"protected", "public", "package", "private"};
}
}
/**
* Holds a collection of ResourceCollections.
*
* <p>A separate kind of container is needed since this task
* contains special handling for FileSets that has to occur at
* task runtime.</p>
*/
public class ResourceCollectionContainer
implements Iterable<ResourceCollection> {
private final List<ResourceCollection> rcs = new ArrayList<>();
/**
* Add a resource collection to the container.
* @param rc the collection to add.
*/
public void add(final ResourceCollection rc) {
rcs.add(rc);
}
/**
* Get an iterator on the collection.
* @return an iterator.
*/
@Override
public Iterator<ResourceCollection> iterator() {
return rcs.iterator();
}
}
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
/** The command line built to execute Javadoc. */
private final Commandline cmd = new Commandline();
/**
* Utility method to add an argument to the command line conditionally
* based on the given flag.
*
* @param b the flag which controls if the argument is added.
* @param arg the argument value.
*/
private void addArgIf(final boolean b, final String arg) {
if (b) {
cmd.createArgument().setValue(arg);
}
}
/**
* Utility method to add a Javadoc argument.
*
* @param key the argument name.
* @param value the argument value.
*/
private void addArgIfNotEmpty(final String key, final String value) {
if (value == null || value.isEmpty()) {
log("Warning: Leaving out empty argument '" + key + "'",
Project.MSG_WARN);
} else {
cmd.createArgument().setValue(key);
cmd.createArgument().setValue(value);
}
}
/**
* Flag which indicates if the task should fail if there is a
* Javadoc error.
*/
private boolean failOnError = false;
/**
* Flag which indicates if the task should fail if there is a
* Javadoc warning.
*/
private boolean failOnWarning = false;
private Path sourcePath = null;
private File destDir = null;
private final List<SourceFile> sourceFiles = new Vector<>();
private final List<PackageName> packageNames = new Vector<>();
private final List<PackageName> excludePackageNames = new Vector<>(1);
private final List<PackageName> moduleNames = new ArrayList<>();
private boolean author = true;
private boolean version = true;
private DocletInfo doclet = null;
private Path classpath = null;
private Path bootclasspath = null;
private Path modulePath = null;
private Path moduleSourcePath = null;
private String group = null;
private String packageList = null;
private final List<LinkArgument> links = new Vector<>();
private final List<GroupArgument> groups = new Vector<>();
private final List<Object> tags = new Vector<>();
private boolean useDefaultExcludes = true;
private Html doctitle = null;
private Html header = null;
private Html footer = null;
private Html bottom = null;
private boolean useExternalFile = false;
private String source = null;
private boolean linksource = false;
private boolean breakiterator = false;
private String noqualifier;
private boolean includeNoSourcePackages = false;
private String executable = null;
private boolean docFilesSubDirs = false;
private String excludeDocFilesSubDir = null;
private String docEncoding = null;
private boolean postProcessGeneratedJavadocs = true;
private final ResourceCollectionContainer nestedSourceFiles
= new ResourceCollectionContainer();
private final List<DirSet> packageSets = new Vector<>();
/**
* Work around command line length limit by using an external file
* for the sourcefiles.
*
* @param b true if an external file is to be used.
*/
public void setUseExternalFile(final boolean b) {
useExternalFile = b;
}
/**
* Sets whether default exclusions should be used or not.
*
* @param useDefaultExcludes "true"|"on"|"yes" when default exclusions
* should be used, "false"|"off"|"no" when they
* shouldn't be used.
*/
public void setDefaultexcludes(final boolean useDefaultExcludes) {
this.useDefaultExcludes = useDefaultExcludes;
}
/**
* Set the maximum memory to be used by the javadoc process
*
* @param max a string indicating the maximum memory according to the
* JVM conventions (e.g. 128m is 128 Megabytes)
*/
public void setMaxmemory(final String max) {
cmd.createArgument().setValue("-J-Xmx" + max);
}
/**
* Set an additional parameter on the command line
*
* @param add the additional command line parameter for the javadoc task.
*/
public void setAdditionalparam(final String add) {
cmd.createArgument().setLine(add);
}
/**
* Adds a command-line argument.
* @return a command-line argument to configure
* @since Ant 1.6
*/
public Commandline.Argument createArg() {
return cmd.createArgument();
}
/**
* Specify where to find source file
*
* @param src a Path instance containing the various source directories.
*/
public void setSourcepath(final Path src) {
if (sourcePath == null) {
sourcePath = src;
} else {
sourcePath.append(src);
}
}
/**
* Create a path to be configured with the locations of the source
* files.
*
* @return a new Path instance to be configured by the Ant core.
*/
public Path createSourcepath() {
if (sourcePath == null) {
sourcePath = new Path(getProject());
}
return sourcePath.createPath();
}
/**
* Adds a reference to a CLASSPATH defined elsewhere.
*
* @param r the reference containing the source path definition.
*/
public void setSourcepathRef(final Reference r) {
createSourcepath().setRefid(r);
}
/**
* Specify where to find modules
*
* @param mp a Path instance containing the modules.
*
* @since Ant 1.10.6
*/
public void setModulePath(final Path mp) {
if (modulePath == null) {
modulePath = mp;
} else {
modulePath.append(mp);
}
}
/**
* Create a path to be configured with the locations of the module
* files.
*
* @return a new Path instance to be configured by the Ant core.
*
* @since Ant 1.10.6
*/
public Path createModulePath() {
if (modulePath == null) {
modulePath = new Path(getProject());
}
return modulePath.createPath();
}
/**
* Adds a reference to a path defined elsewhere that defines the module path.
*
* @param r the reference containing the module path definition.
*
* @since Ant 1.10.6
*/
public void setModulePathref(final Reference r) {
createModulePath().setRefid(r);
}
/**
* Specify where to find sources for modules
*
* @param mp a Path instance containing the sources for modules.
*
* @since Ant 1.10.6
*/
public void setModuleSourcePath(final Path mp) {
if (moduleSourcePath == null) {
moduleSourcePath = mp;
} else {
moduleSourcePath.append(mp);
}
}
/**
* Create a path to be configured with the locations of the module
* source files.
*
* @return a new Path instance to be configured by the Ant core.
*
* @since Ant 1.10.6
*/
public Path createModuleSourcePath() {
if (moduleSourcePath == null) {
moduleSourcePath = new Path(getProject());
}
return moduleSourcePath.createPath();
}
/**
* Adds a reference to a path defined elsewhere that defines the module source path.
*
* @param r the reference containing the module source path definition.
*
* @since Ant 1.10.6
*/
public void setModuleSourcePathref(final Reference r) {
createModuleSourcePath().setRefid(r);
}
/**
* Set the directory where the Javadoc output will be generated.
*
* @param dir the destination directory.
*/
public void setDestdir(final File dir) {
destDir = dir;
cmd.createArgument().setValue("-d");
cmd.createArgument().setFile(destDir);
}
/**
* Set the list of source files to process.
*
* @param src a comma separated list of source files.
*/
public void setSourcefiles(final String src) {
final StringTokenizer tok = new StringTokenizer(src, ",");
while (tok.hasMoreTokens()) {
final String f = tok.nextToken();
final SourceFile sf = new SourceFile();
sf.setFile(getProject().resolveFile(f.trim()));
addSource(sf);
}
}
/**
* Add a single source file.
*
* @param sf the source file to be processed.
*/
public void addSource(final SourceFile sf) {
sourceFiles.add(sf);
}
/**
* Set the package names to be processed.
*
* @param packages a comma separated list of packages specs
* (may be wildcarded).
*
* @see #addPackage for wildcard information.
*/
public void setPackagenames(final String packages) {
final StringTokenizer tok = new StringTokenizer(packages, ",");
while (tok.hasMoreTokens()) {
final String p = tok.nextToken();
final PackageName pn = new PackageName();
pn.setName(p);
addPackage(pn);
}
}
/**
* Set the module names to be processed.
*
* @param modules a comma separated list of module names
*
* @since Ant 1.10.6
*/
public void setModulenames(final String modules) {
for (String m : modules.split(",")) {
final PackageName mn = new PackageName();
mn.setName(m);
addModule(mn);
}
}
/**
* Add a single package to be processed.
*
* If the package name ends with &quot;.*&quot; the Javadoc task
* will find and process all subpackages.
*
* @param pn the package name, possibly wildcarded.
*/
public void addPackage(final PackageName pn) {
packageNames.add(pn);
}
/**
* Add a single module to be processed.
*
* @param mn the module name
*
* @since Ant 1.10.6
*/
public void addModule(final PackageName mn) {
moduleNames.add(mn);
}
/**
* Set the list of packages to be excluded.
*
* @param packages a comma separated list of packages to be excluded.
* This may not include wildcards.
*/
public void setExcludePackageNames(final String packages) {
final StringTokenizer tok = new StringTokenizer(packages, ",");
while (tok.hasMoreTokens()) {
final String p = tok.nextToken();
final PackageName pn = new PackageName();
pn.setName(p);
addExcludePackage(pn);
}
}
/**
* Add a package to be excluded from the Javadoc run.
*
* @param pn the name of the package (wildcards are not permitted).
*/
public void addExcludePackage(final PackageName pn) {
excludePackageNames.add(pn);
}
/**
* Specify the file containing the overview to be included in the generated
* documentation.
*
* @param f the file containing the overview.
*/
public void setOverview(final File f) {
cmd.createArgument().setValue("-overview");
cmd.createArgument().setFile(f);
}
/**
* Indicate whether only public classes and members are to be included in
* the scope processed
*
* @param b true if scope is to be public.
*/
public void setPublic(final boolean b) {
addArgIf(b, "-public");
}
/**
* Indicate whether only protected and public classes and members are to
* be included in the scope processed
*
* @param b true if scope is to be protected.
*/
public void setProtected(final boolean b) {
addArgIf(b, "-protected");
}
/**
* Indicate whether only package, protected and public classes and
* members are to be included in the scope processed
*
* @param b true if scope is to be package level.
*/
public void setPackage(final boolean b) {
addArgIf(b, "-package");
}
/**
* Indicate whether all classes and
* members are to be included in the scope processed
*
* @param b true if scope is to be private level.
*/
public void setPrivate(final boolean b) {
addArgIf(b, "-private");
}
/**
* Set the scope to be processed. This is an alternative to the
* use of the setPublic, setPrivate, etc methods. It gives better build
* file control over what scope is processed.
*
* @param at the scope to be processed.
*/
public void setAccess(final AccessType at) {
cmd.createArgument().setValue("-" + at.getValue());
}
/**
* Set the class that starts the doclet used in generating the
* documentation.
*
* @param docletName the name of the doclet class.
*/
public void setDoclet(final String docletName) {
if (doclet == null) {
doclet = new DocletInfo();
doclet.setProject(getProject());
}
doclet.setName(docletName);
}
/**
* Set the classpath used to find the doclet class.
*
* @param docletPath the doclet classpath.
*/
public void setDocletPath(final Path docletPath) {
if (doclet == null) {
doclet = new DocletInfo();
doclet.setProject(getProject());
}
doclet.setPath(docletPath);
}
/**
* Set the classpath used to find the doclet class by reference.
*
* @param r the reference to the Path instance to use as the doclet
* classpath.
*/
public void setDocletPathRef(final Reference r) {
if (doclet == null) {
doclet = new DocletInfo();
doclet.setProject(getProject());
}
doclet.createPath().setRefid(r);
}
/**
* Create a doclet to be used in the documentation generation.
*
* @return a new DocletInfo instance to be configured.
*/
public DocletInfo createDoclet() {
if (doclet == null) {
doclet = new DocletInfo();
}
return doclet;
}
/**
* Add a taglet
*
* @param tagletInfo information about the taglet.
*/
public void addTaglet(final ExtensionInfo tagletInfo) {
tags.add(tagletInfo);
}
/**
* Indicate whether Javadoc should produce old style (JDK 1.1)
* documentation.
*
* This is not supported by JDK 1.1 and has been phased out in JDK 1.4
*
* @param b if true attempt to generate old style documentation.
*/
public void setOld(final boolean b) {
log("Javadoc 1.4 doesn't support the -1.1 switch anymore",
Project.MSG_WARN);
}
/**
* Set the classpath to be used for this Javadoc run.
*
* @param path an Ant Path object containing the compilation
* classpath.
*/
public void setClasspath(final Path path) {
if (classpath == null) {
classpath = path;
} else {
classpath.append(path);
}
}
/**
* Create a Path to be configured with the classpath to use
*
* @return a new Path instance to be configured with the classpath.
*/
public Path createClasspath() {
if (classpath == null) {
classpath = new Path(getProject());
}
return classpath.createPath();
}
/**
* Adds a reference to a CLASSPATH defined elsewhere.
*
* @param r the reference to an instance defining the classpath.
*/
public void setClasspathRef(final Reference r) {
createClasspath().setRefid(r);
}
/**
* Set the boot classpath to use.
*
* @param path the boot classpath.
*/
public void setBootclasspath(final Path path) {
if (bootclasspath == null) {
bootclasspath = path;
} else {
bootclasspath.append(path);
}
}
/**
* Create a Path to be configured with the boot classpath
*
* @return a new Path instance to be configured with the boot classpath.
*/
public Path createBootclasspath() {
if (bootclasspath == null) {
bootclasspath = new Path(getProject());
}
return bootclasspath.createPath();
}
/**
* Adds a reference to a CLASSPATH defined elsewhere.
*
* @param r the reference to an instance defining the bootclasspath.
*/
public void setBootClasspathRef(final Reference r) {
createBootclasspath().setRefid(r);
}
/**
* Set the location of the extensions directories.
*
* @param path the string version of the path.
* @deprecated since 1.5.x.
* Use the {@link #setExtdirs(Path)} version.
*/
@Deprecated
public void setExtdirs(final String path) {
cmd.createArgument().setValue("-extdirs");
cmd.createArgument().setValue(path);
}
/**
* Set the location of the extensions directories.
*
* @param path a path containing the extension directories.
*/
public void setExtdirs(final Path path) {
cmd.createArgument().setValue("-extdirs");
cmd.createArgument().setPath(path);
}
/**
* Run javadoc in verbose mode
*
* @param b true if operation is to be verbose.
*/
public void setVerbose(final boolean b) {
addArgIf(b, "-verbose");
}
/**
* Set the local to use in documentation generation.
*
* @param locale the locale to use.
*/
public void setLocale(final String locale) {
// createArgument(true) is necessary to make sure -locale
// is the first argument (required in 1.3+).
cmd.createArgument(true).setValue(locale);
cmd.createArgument(true).setValue("-locale");
}
/**
* Set the encoding name of the source files,
*
* @param enc the name of the encoding for the source files.
*/
public void setEncoding(final String enc) {
cmd.createArgument().setValue("-encoding");
cmd.createArgument().setValue(enc);
}
/**
* Include the version tag in the generated documentation.
*
* @param b true if the version tag should be included.
*/
public void setVersion(final boolean b) {
this.version = b;
}
/**
* Generate the &quot;use&quot; page for each package.
*
* @param b true if the use page should be generated.
*/
public void setUse(final boolean b) {
addArgIf(b, "-use");
}
/**
* Include the author tag in the generated documentation.
*
* @param b true if the author tag should be included.
*/
public void setAuthor(final boolean b) {
author = b;
}
/**
* Generate a split index
*
* @param b true if the index should be split into a file per letter.
*/
public void setSplitindex(final boolean b) {
addArgIf(b, "-splitindex");
}
/**
* Set the title to be placed in the HTML &lt;title&gt; tag of the
* generated documentation.
*
* @param title the window title to use.
*/
public void setWindowtitle(final String title) {
addArgIfNotEmpty("-windowtitle", title);
}
/**
* Set the title of the generated overview page.
*
* @param doctitle the Document title.
*/
public void setDoctitle(final String doctitle) {
final Html h = new Html();
h.addText(doctitle);
addDoctitle(h);
}
/**
* Add a document title to use for the overview page.
*
* @param text the HTML element containing the document title.
*/
public void addDoctitle(final Html text) {
doctitle = text;
}
/**
* Set the header text to be placed at the top of each output file.
*
* @param header the header text
*/
public void setHeader(final String header) {
final Html h = new Html();
h.addText(header);
addHeader(h);
}
/**
* Set the header text to be placed at the top of each output file.
*
* @param text the header text
*/
public void addHeader(final Html text) {
header = text;
}
/**
* Set the footer text to be placed at the bottom of each output file.
*
* @param footer the footer text.
*/
public void setFooter(final String footer) {
final Html h = new Html();
h.addText(footer);
addFooter(h);
}
/**
* Set the footer text to be placed at the bottom of each output file.
*
* @param text the footer text.
*/
public void addFooter(final Html text) {
footer = text;
}
/**
* Set the text to be placed at the bottom of each output file.
*
* @param bottom the bottom text.
*/
public void setBottom(final String bottom) {
final Html h = new Html();
h.addText(bottom);
addBottom(h);
}
/**
* Set the text to be placed at the bottom of each output file.
*
* @param text the bottom text.
*/
public void addBottom(final Html text) {
bottom = text;
}
/**
* Link to docs at "url" using package list at "url2"
* - separate the URLs by using a space character.
*
* @param src the offline link specification (url and package list)
*/
public void setLinkoffline(final String src) {
final LinkArgument le = createLink();
le.setOffline(true);
final String linkOfflineError = "The linkoffline attribute must include"
+ " a URL and a package-list file location separated by a"
+ " space";
if (src.trim().isEmpty()) {
throw new BuildException(linkOfflineError);
}
final StringTokenizer tok = new StringTokenizer(src, " ", false);
le.setHref(tok.nextToken());
if (!tok.hasMoreTokens()) {
throw new BuildException(linkOfflineError);
}
le.setPackagelistLoc(getProject().resolveFile(tok.nextToken()));
}
/**
* Group specified packages together in overview page.
*
* @param src the group packages - a command separated list of group specs,
* each one being a group name and package specification separated
* by a space.
*/
public void setGroup(final String src) {
group = src;
}
/**
* Create links to Javadoc output at the given URL.
* @param src the URL to link to
*/
public void setLink(final String src) {
createLink().setHref(src);
}
/**
* Control deprecation information
*
* @param b If true, do not include deprecated information.
*/
public void setNodeprecated(final boolean b) {
addArgIf(b, "-nodeprecated");
}
/**
* Control deprecated list generation
*
* @param b if true, do not generate deprecated list.
*/
public void setNodeprecatedlist(final boolean b) {
addArgIf(b, "-nodeprecatedlist");
}
/**
* Control class tree generation.
*
* @param b if true, do not generate class hierarchy.
*/
public void setNotree(final boolean b) {
addArgIf(b, "-notree");
}
/**
* Control generation of index.
*
* @param b if true, do not generate index.
*/
public void setNoindex(final boolean b) {
addArgIf(b, "-noindex");
}
/**
* Control generation of help link.
*
* @param b if true, do not generate help link
*/
public void setNohelp(final boolean b) {
addArgIf(b, "-nohelp");
}
/**
* Control generation of the navigation bar.
*
* @param b if true, do not generate navigation bar.
*/
public void setNonavbar(final boolean b) {
addArgIf(b, "-nonavbar");
}
/**
* Control warnings about serial tag.
*
* @param b if true, generate warning about the serial tag.
*/
public void setSerialwarn(final boolean b) {
addArgIf(b, "-serialwarn");
}
/**
* Specifies the CSS stylesheet file to use.
*
* @param f the file with the CSS to use.
*/
public void setStylesheetfile(final File f) {
cmd.createArgument().setValue("-stylesheetfile");
cmd.createArgument().setFile(f);
}
/**
* Specifies the HTML help file to use.
*
* @param f the file containing help content.
*/
public void setHelpfile(final File f) {
cmd.createArgument().setValue("-helpfile");
cmd.createArgument().setFile(f);
}
/**
* Output file encoding name.
*
* @param enc name of the encoding to use.
*/
public void setDocencoding(final String enc) {
cmd.createArgument().setValue("-docencoding");
cmd.createArgument().setValue(enc);
docEncoding = enc;
}
/**
* The name of a file containing the packages to process.
*
* @param src the file containing the package list.
*/
public void setPackageList(final String src) {
packageList = src;
}
/**
* Create link to Javadoc output at the given URL.
*
* @return link argument to configure
*/
public LinkArgument createLink() {
final LinkArgument la = new LinkArgument();
links.add(la);
return la;
}
/**
* Represents a link triplet (href, whether link is offline,
* location of the package list if off line)
*/
public class LinkArgument {
private String href;
private boolean offline = false;
private File packagelistLoc;
private URL packagelistURL;
private boolean resolveLink = false;
/** Constructor for LinkArgument */
public LinkArgument() {
//empty
}
/**
* Set the href attribute.
* @param hr a <code>String</code> value
*/
public void setHref(final String hr) {
href = hr;
}
/**
* Get the href attribute.
* @return the href attribute.
*/
public String getHref() {
return href;
}
/**
* Set the packetlist location attribute.
* @param src a <code>File</code> value
*/
public void setPackagelistLoc(final File src) {
packagelistLoc = src;
}
/**
* Get the packetList location attribute.
* @return the packetList location attribute.
*/
public File getPackagelistLoc() {
return packagelistLoc;
}
/**
* Set the packetlist location attribute.
* @param src an <code>URL</code> value
*/
public void setPackagelistURL(final URL src) {
packagelistURL = src;
}
/**
* Get the packetList location attribute.
* @return the packetList location attribute.
*/
public URL getPackagelistURL() {
return packagelistURL;
}
/**
* Set the offline attribute.
* @param offline a <code>boolean</code> value
*/
public void setOffline(final boolean offline) {
this.offline = offline;
}
/**
* Get the linkOffline attribute.
* @return the linkOffline attribute.
*/
public boolean isLinkOffline() {
return offline;
}
/**
* Sets whether Ant should resolve the link attribute relative
* to the current basedir.
* @param resolve a <code>boolean</code> value
*/
public void setResolveLink(final boolean resolve) {
this.resolveLink = resolve;
}
/**
* should Ant resolve the link attribute relative to the
* current basedir?
* @return the resolveLink attribute.
*/
public boolean shouldResolveLink() {
return resolveLink;
}
}
/**
* Creates and adds a -tag argument. This is used to specify
* custom tags. This argument is only available for Javadoc 1.4,
* and will generate a verbose message (and then be ignored)
* when run on Java versions below 1.4.
* @return tag argument to be configured
*/
public TagArgument createTag() {
final TagArgument ta = new TagArgument();
tags.add(ta);
return ta;
}
/**
* Scope element verbose names. (Defined here as fields
* cannot be static in inner classes.) The first letter
* from each element is used to build up the scope string.
*/
static final String[] SCOPE_ELEMENTS = { //NOSONAR
"overview", "packages", "types", "constructors",
"methods", "fields"
};
/**
* Class representing a -tag argument.
*/
public class TagArgument extends FileSet {
/** Name of the tag. */
private String name = null;
/** Whether or not the tag is enabled. */
private boolean enabled = true;
/**
* Scope string of the tag. This will form the middle
* argument of the -tag parameter when the tag is enabled
* (with an X prepended for and is parsed from human-readable form.
*/
private String scope = "a";
/** Sole constructor. */
public TagArgument() {
//empty
}
/**
* Sets the name of the tag.
*
* @param name The name of the tag.
* Must not be <code>null</code> or empty.
*/
public void setName(final String name) {
this.name = name;
}
/**
* Sets the scope of the tag. This is in comma-separated
* form, with each element being one of "all" (the default),
* "overview", "packages", "types", "constructors", "methods",
* "fields". The elements are treated in a case-insensitive
* manner.
*
* @param verboseScope The scope of the tag.
* Must not be <code>null</code>,
* should not be empty.
*
* @exception BuildException if all is specified along with
* other elements, if any elements are repeated, if no
* elements are specified, or if any unrecognised elements are
* specified.
*/
public void setScope(String verboseScope) throws BuildException {
verboseScope = verboseScope.toLowerCase(Locale.ENGLISH);
final boolean[] elements = new boolean[SCOPE_ELEMENTS.length];
boolean gotAll = false;
boolean gotNotAll = false;
// Go through the tokens one at a time, updating the
// elements array and issuing warnings where appropriate.
final StringTokenizer tok = new StringTokenizer(verboseScope, ",");
while (tok.hasMoreTokens()) {
final String next = tok.nextToken().trim();
if ("all".equals(next)) {
if (gotAll) {
getProject().log("Repeated tag scope element: all",
Project.MSG_VERBOSE);
}
gotAll = true;
} else {
int i;
for (i = 0; i < SCOPE_ELEMENTS.length; i++) {
if (SCOPE_ELEMENTS[i].equals(next)) {
break;
}
}
if (i == SCOPE_ELEMENTS.length) {
throw new BuildException(
"Unrecognised scope element: %s", next);
}
if (elements[i]) {
getProject().log("Repeated tag scope element: " + next,
Project.MSG_VERBOSE);
}
elements[i] = true;
gotNotAll = true;
}
}
if (gotNotAll && gotAll) {
throw new BuildException(
"Mixture of \"all\" and other scope elements in tag parameter.");
}
if (!gotNotAll && !gotAll) {
throw new BuildException(
"No scope elements specified in tag parameter.");
}
if (gotAll) {
this.scope = "a";
} else {
final StringBuilder buff = new StringBuilder(elements.length);
for (int i = 0; i < elements.length; i++) {
if (elements[i]) {
buff.append(SCOPE_ELEMENTS[i].charAt(0));
}
}
this.scope = buff.toString();
}
}
/**
* Sets whether or not the tag is enabled.
*
* @param enabled Whether or not this tag is enabled.
*/
public void setEnabled(final boolean enabled) {
this.enabled = enabled;
}
/**
* Returns the -tag parameter this argument represented.
* @return the -tag parameter as a string
* @exception BuildException if either the name or description
* is <code>null</code> or empty.
*/
public String getParameter() throws BuildException {
if (name == null || name.isEmpty()) {
throw new BuildException("No name specified for custom tag.");
}
if (getDescription() != null) {
return name + ":" + (enabled ? "" : "X")
+ scope + ":" + getDescription();
}
if (!enabled || !"a".equals(scope)) {
return name + ":" + (enabled ? "" : "X") + scope;
}
return name;
}
}
/**
* Separates packages on the overview page into whatever
* groups you specify, one group per table.
* @return a group argument to be configured
*/
public GroupArgument createGroup() {
final GroupArgument ga = new GroupArgument();
groups.add(ga);
return ga;
}
/**
* A class corresponding to the group nested element.
*/
public class GroupArgument {
private Html title;
private final List<PackageName> packages = new Vector<>();
/**
* Set the title attribute using a string.
* @param src a <code>String</code> value
*/
public void setTitle(final String src) {
final Html h = new Html();
h.addText(src);
addTitle(h);
}
/**
* Set the title attribute using a nested Html value.
* @param text a <code>Html</code> value
*/
public void addTitle(final Html text) {
title = text;
}
/**
* Get the title.
* @return the title
*/
public String getTitle() {
return title != null ? title.getText() : null;
}
/**
* Set the packages to Javadoc on.
* @param src a comma separated list of packages
*/
public void setPackages(final String src) {
final StringTokenizer tok = new StringTokenizer(src, ",");
while (tok.hasMoreTokens()) {
final String p = tok.nextToken();
final PackageName pn = new PackageName();
pn.setName(p);
addPackage(pn);
}
}
/**
* Add a package nested element.
* @param pn a nested element specifying the package.
*/
public void addPackage(final PackageName pn) {
packages.add(pn);
}
/**
* Get the packages as a colon separated list.
* @return the packages as a string
*/
public String getPackages() {
return packages.stream().map(Object::toString)
.collect(Collectors.joining(":"));
}
}
/**
* Charset for cross-platform viewing of generated documentation.
* @param src the name of the charset
*/
public void setCharset(final String src) {
this.addArgIfNotEmpty("-charset", src);
}
/**
* Should the build process fail if Javadoc fails (as indicated by
* a non zero return code)?
*
* <p>Default is false.</p>
* @param b a <code>boolean</code> value
*/
public void setFailonerror(final boolean b) {
failOnError = b;
}
/**
* Should the build process fail if Javadoc warns (as indicated by
* the word "warning" on stdout)?
*
* <p>Default is false.</p>
* @param b a <code>boolean</code> value
* @since Ant 1.9.4
*/
public void setFailonwarning(final boolean b) {
failOnWarning = b;
}
/**
* Enables the -source switch, will be ignored if Javadoc is not
* the 1.4 version.
* @param source a <code>String</code> value
* @since Ant 1.5
*/
public void setSource(final String source) {
this.source = source;
}
/**
* Sets the actual executable command to invoke, instead of the binary
* <code>javadoc</code> found in Ant's JDK.
* @param executable the command to invoke.
* @since Ant 1.6.3
*/
public void setExecutable(final String executable) {
this.executable = executable;
}
/**
* Adds a packageset.
*
* <p>All included directories will be translated into package
* names be converting the directory separator into dots.</p>
* @param packageSet a directory set
* @since 1.5
*/
public void addPackageset(final DirSet packageSet) {
packageSets.add(packageSet);
}
/**
* Adds a fileset.
*
* <p>All included files will be added as sourcefiles. The task
* will automatically add
* <code>includes=&quot;**&#47;*.java&quot;</code> to the
* fileset.</p>
* @param fs a file set
* @since 1.5
*/
public void addFileset(final FileSet fs) {
createSourceFiles().add(fs);
}
/**
* Adds a container for resource collections.
*
* <p>All included files will be added as sourcefiles.</p>
* @return the source files to configure.
* @since 1.7
*/
public ResourceCollectionContainer createSourceFiles() {
return nestedSourceFiles;
}
/**
* Enables the -linksource switch, will be ignored if Javadoc is not
* the 1.4 version. Default is false
* @param b a <code>String</code> value
* @since Ant 1.6
*/
public void setLinksource(final boolean b) {
this.linksource = b;
}
/**
* Enables the -linksource switch, will be ignored if Javadoc is not
* the 1.4 version. Default is false
* @param b a <code>String</code> value
* @since Ant 1.6
*/
public void setBreakiterator(final boolean b) {
this.breakiterator = b;
}
/**
* Enables the -noqualifier switch, will be ignored if Javadoc is not
* the 1.4 version.
* @param noqualifier the parameter to the -noqualifier switch
* @since Ant 1.6
*/
public void setNoqualifier(final String noqualifier) {
this.noqualifier = noqualifier;
}
/**
* If set to true, Ant will also accept packages that only hold
* package.html files but no Java sources.
* @param b a <code>boolean</code> value.
* @since Ant 1.6.3
*/
public void setIncludeNoSourcePackages(final boolean b) {
this.includeNoSourcePackages = b;
}
/**
* Enables deep-copying of <code>doc-files</code> directories.
*
* @param b boolean
* @since Ant 1.8.0
*/
public void setDocFilesSubDirs(final boolean b) {
docFilesSubDirs = b;
}
/**
* Colon-separated list of <code>doc-files</code> subdirectories
* to skip if {@link #setDocFilesSubDirs docFilesSubDirs is true}.
*
* @param s String
* @since Ant 1.8.0
*/
public void setExcludeDocFilesSubDir(final String s) {
excludeDocFilesSubDir = s;
}
/**
* Whether to post-process the generated javadocs in order to mitigate CVE-2013-1571.
*
* @param b boolean
* @since Ant 1.9.2
*/
public void setPostProcessGeneratedJavadocs(final boolean b) {
postProcessGeneratedJavadocs = b;
}
/**
* Execute the task.
* @throws BuildException on error
*/
@Override
public void execute() throws BuildException {
checkTaskName();
final List<String> packagesToDoc = new Vector<>();
final Path sourceDirs = new Path(getProject());
checkPackageAndSourcePath();
if (sourcePath != null) {
sourceDirs.addExisting(sourcePath);
}
parsePackages(packagesToDoc, sourceDirs);
checkPackages(packagesToDoc, sourceDirs);
final List<SourceFile> sourceFilesToDoc = new ArrayList<>(sourceFiles);
addSourceFiles(sourceFilesToDoc);
checkPackagesToDoc(packagesToDoc, sourceFilesToDoc);
log("Generating Javadoc", Project.MSG_INFO);
final Commandline toExecute = (Commandline) cmd.clone();
if (executable != null) {
toExecute.setExecutable(executable);
} else {
toExecute.setExecutable(JavaEnvUtils.getJdkExecutable("javadoc"));
}
// Javadoc arguments
generalJavadocArguments(toExecute); // general Javadoc arguments
doSourcePath(toExecute, sourceDirs); // sourcepath
doDoclet(toExecute); // arguments for default doclet
doBootPath(toExecute); // bootpath
doLinks(toExecute); // links arguments
doGroup(toExecute); // group attribute
doGroups(toExecute); // groups attribute
doDocFilesSubDirs(toExecute); // docfilessubdir attribute
doModuleArguments(toExecute);
doTags(toExecute);
doSource(toExecute);
doLinkSource(toExecute);
doNoqualifier(toExecute);
if (breakiterator) {
toExecute.createArgument().setValue("-breakiterator");
}
// If using an external file, write the command line options to it
if (useExternalFile) {
writeExternalArgs(toExecute);
}
File tmpList = null;
FileWriter wr = null;
try {
/**
* Write sourcefiles and package names to a temporary file
* if requested.
*/
BufferedWriter srcListWriter = null;
if (useExternalFile) {
tmpList = FILE_UTILS.createTempFile(getProject(), "javadoc", "", null, true, true);
toExecute.createArgument()
.setValue("@" + tmpList.getAbsolutePath());
wr = new FileWriter(tmpList.getAbsolutePath(), true);
srcListWriter = new BufferedWriter(wr);
}
doSourceAndPackageNames(
toExecute, packagesToDoc, sourceFilesToDoc,
useExternalFile, tmpList, srcListWriter);
if (useExternalFile) {
srcListWriter.flush(); //NOSONAR
}
} catch (final IOException e) {
if (tmpList != null) {
tmpList.delete();
}
throw new BuildException("Error creating temporary file",
e, getLocation());
} finally {
FileUtils.close(wr);
}
if (packageList != null) {
toExecute.createArgument().setValue("@" + packageList);
}
log(toExecute.describeCommand(), Project.MSG_VERBOSE);
log("Javadoc execution", Project.MSG_INFO);
final JavadocOutputStream out = new JavadocOutputStream(Project.MSG_INFO);
final JavadocOutputStream err = new JavadocOutputStream(Project.MSG_WARN);
final Execute exe = new Execute(new PumpStreamHandler(out, err));
exe.setAntRun(getProject());
/*
* No reason to change the working directory as all filenames and
* path components have been resolved already.
*
* Avoid problems with command line length in some environments.
*/
exe.setWorkingDirectory(null);
try {
exe.setCommandline(toExecute.getCommandline());
final int ret = exe.execute();
if (ret != 0 && failOnError) {
throw new BuildException("Javadoc returned " + ret,
getLocation());
}
if (out.sawWarnings() && failOnWarning) {
throw new BuildException("Javadoc issued warnings.",
getLocation());
}
postProcessGeneratedJavadocs();
} catch (final IOException e) {
throw new BuildException("Javadoc failed: " + e, e, getLocation());
} finally {
if (tmpList != null) {
tmpList.delete();
tmpList = null;
}
out.logFlush();
err.logFlush();
FileUtils.close(out);
FileUtils.close(err);
}
}
private void checkTaskName() {
if ("javadoc2".equals(getTaskType())) {
log("Warning: the task name <javadoc2> is deprecated."
+ " Use <javadoc> instead.",
Project.MSG_WARN);
}
}
private void checkPackageAndSourcePath() {
if (packageList != null && sourcePath == null) {
final String msg = "sourcePath attribute must be set when "
+ "specifying packagelist.";
throw new BuildException(msg);
}
}
private void checkPackages(final List<String> packagesToDoc, final Path sourceDirs) {
if (!packagesToDoc.isEmpty() && sourceDirs.isEmpty()) {
throw new BuildException(
"sourcePath attribute must be set when specifying package names.");
}
}
private void checkPackagesToDoc(
final List<String> packagesToDoc, final List<SourceFile> sourceFilesToDoc) {
if (packageList == null && packagesToDoc.isEmpty()
&& sourceFilesToDoc.isEmpty() && moduleNames.isEmpty()) {
throw new BuildException("No source files, no packages and no modules have "
+ "been specified.");
}
}
private void doSourcePath(final Commandline toExecute, final Path sourceDirs) {
if (!sourceDirs.isEmpty()) {
toExecute.createArgument().setValue("-sourcepath");
toExecute.createArgument().setPath(sourceDirs);
}
}
private void generalJavadocArguments(final Commandline toExecute) {
if (doctitle != null) {
toExecute.createArgument().setValue("-doctitle");
toExecute.createArgument().setValue(expand(doctitle.getText()));
}
if (header != null) {
toExecute.createArgument().setValue("-header");
toExecute.createArgument().setValue(expand(header.getText()));
}
if (footer != null) {
toExecute.createArgument().setValue("-footer");
toExecute.createArgument().setValue(expand(footer.getText()));
}
if (bottom != null) {
toExecute.createArgument().setValue("-bottom");
toExecute.createArgument().setValue(expand(bottom.getText()));
}
if (classpath == null) {
classpath = new Path(getProject()).concatSystemClasspath("last");
} else {
classpath = classpath.concatSystemClasspath("ignore");
}
if (classpath.size() > 0) {
toExecute.createArgument().setValue("-classpath");
toExecute.createArgument().setPath(classpath);
}
if (version && doclet == null) {
toExecute.createArgument().setValue("-version");
}
if (author && doclet == null) {
toExecute.createArgument().setValue("-author");
}
if (doclet == null && destDir == null) {
throw new BuildException("destdir attribute must be set!");
}
}
private void doDoclet(final Commandline toExecute) {
if (doclet != null) {
if (doclet.getName() == null) {
throw new BuildException("The doclet name must be specified.", getLocation());
}
toExecute.createArgument().setValue("-doclet");
toExecute.createArgument().setValue(doclet.getName());
if (doclet.getPath() != null) {
final Path docletPath = doclet.getPath().concatSystemClasspath("ignore");
if (docletPath.size() != 0) {
toExecute.createArgument().setValue("-docletpath");
toExecute.createArgument().setPath(docletPath);
}
}
for (final DocletParam param : Collections.list(doclet.getParams())) {
if (param.getName() == null) {
throw new BuildException("Doclet parameters must have a name");
}
toExecute.createArgument().setValue(param.getName());
if (param.getValue() != null) {
toExecute.createArgument().setValue(param.getValue());
}
}
}
}
private void writeExternalArgs(final Commandline toExecute) {
// If using an external file, write the command line options to it
File optionsTmpFile = null;
try {
optionsTmpFile = FILE_UTILS.createTempFile(
getProject(), "javadocOptions", "", null, true, true);
final String[] listOpt = toExecute.getArguments();
toExecute.clearArgs();
toExecute.createArgument().setValue(
"@" + optionsTmpFile.getAbsolutePath());
try (BufferedWriter optionsListWriter = new BufferedWriter(
new FileWriter(optionsTmpFile.getAbsolutePath(), true))) {
for (final String opt : listOpt) {
if (opt.startsWith("-J-")) {
toExecute.createArgument().setValue(opt);
} else if (opt.startsWith("-")) {
optionsListWriter.write(opt);
optionsListWriter.write(" ");
} else {
optionsListWriter.write(quoteString(opt));
optionsListWriter.newLine();
}
}
}
} catch (final IOException ex) {
if (optionsTmpFile != null) {
optionsTmpFile.delete();
}
throw new BuildException(
"Error creating or writing temporary file for javadoc options",
ex, getLocation());
}
}
private void doBootPath(final Commandline toExecute) {
Path bcp = new Path(getProject());
if (bootclasspath != null) {
bcp.append(bootclasspath);
}
bcp = bcp.concatSystemBootClasspath("ignore");
if (bcp.size() > 0) {
toExecute.createArgument().setValue("-bootclasspath");
toExecute.createArgument().setPath(bcp);
}
}
private void doLinks(final Commandline toExecute) {
for (final LinkArgument la : links) {
if (la.getHref() == null || la.getHref().isEmpty()) {
log("No href was given for the link - skipping",
Project.MSG_VERBOSE);
continue;
}
String link = null;
if (la.shouldResolveLink()) {
final File hrefAsFile =
getProject().resolveFile(la.getHref());
if (hrefAsFile.exists()) {
try {
link = FILE_UTILS.getFileURL(hrefAsFile)
.toExternalForm();
} catch (final MalformedURLException ex) {
// should be impossible
log("Warning: link location was invalid "
+ hrefAsFile, Project.MSG_WARN);
}
}
}
if (link == null) {
// is the href a valid URL
try {
final URL base = new URL("file://.");
// created for the side effect of throwing a MalformedURLException
new URL(base, la.getHref()); //NOSONAR
link = la.getHref();
} catch (final MalformedURLException mue) {
// ok - just skip
log("Link href \"" + la.getHref()
+ "\" is not a valid url - skipping link",
Project.MSG_WARN);
continue;
}
}
if (la.isLinkOffline()) {
final File packageListLocation = la.getPackagelistLoc();
URL packageListURL = la.getPackagelistURL();
if (packageListLocation == null
&& packageListURL == null) {
throw new BuildException(
"The package list location for link " + la.getHref()
+ " must be provided because the link is offline");
}
if (packageListLocation != null) {
final File packageListFile =
new File(packageListLocation, "package-list");
if (packageListFile.exists()) {
try {
packageListURL =
FILE_UTILS.getFileURL(packageListLocation);
} catch (final MalformedURLException ex) {
log("Warning: Package list location was "
+ "invalid " + packageListLocation,
Project.MSG_WARN);
}
} else {
log("Warning: No package list was found at "
+ packageListLocation, Project.MSG_VERBOSE);
}
}
if (packageListURL != null) {
toExecute.createArgument().setValue("-linkoffline");
toExecute.createArgument().setValue(link);
toExecute.createArgument()
.setValue(packageListURL.toExternalForm());
}
} else {
toExecute.createArgument().setValue("-link");
toExecute.createArgument().setValue(link);
}
}
}
private void doGroup(final Commandline toExecute) {
// add the single group arguments
// Javadoc 1.2 rules:
// Multiple -group args allowed.
// Each arg includes 3 strings: -group [name] [packagelist].
// Elements in [packagelist] are colon-delimited.
// An element in [packagelist] may end with the * wildcard.
// Ant javadoc task rules for group attribute:
// Args are comma-delimited.
// Each arg is 2 space-delimited strings.
// E.g., group="XSLT_Packages org.apache.xalan.xslt*,
// XPath_Packages org.apache.xalan.xpath*"
if (group != null) {
final StringTokenizer tok = new StringTokenizer(group, ",", false);
while (tok.hasMoreTokens()) {
final String grp = tok.nextToken().trim();
final int space = grp.indexOf(' ');
if (space > 0) {
final String name = grp.substring(0, space);
final String pkgList = grp.substring(space + 1);
toExecute.createArgument().setValue("-group");
toExecute.createArgument().setValue(name);
toExecute.createArgument().setValue(pkgList);
}
}
}
}
// add the group arguments
private void doGroups(final Commandline toExecute) {
for (final GroupArgument ga : groups) {
final String title = ga.getTitle();
final String packages = ga.getPackages();
if (title == null || packages == null) {
throw new BuildException(
"The title and packages must be specified for group elements.");
}
toExecute.createArgument().setValue("-group");
toExecute.createArgument().setValue(expand(title));
toExecute.createArgument().setValue(packages);
}
}
private void doNoqualifier(final Commandline toExecute) {
if (noqualifier != null && doclet == null) {
toExecute.createArgument().setValue("-noqualifier");
toExecute.createArgument().setValue(noqualifier);
}
}
private void doLinkSource(final Commandline toExecute) {
if (linksource && doclet == null) {
toExecute.createArgument().setValue("-linksource");
}
}
private void doSource(final Commandline toExecute) {
final String sourceArg = source != null ? source : getProject().getProperty(MagicNames.BUILD_JAVAC_SOURCE);
if (sourceArg != null) {
toExecute.createArgument().setValue("-source");
toExecute.createArgument().setValue(sourceArg);
}
}
private void doTags(final Commandline toExecute) {
for (final Object element : tags) {
if (element instanceof TagArgument) {
final TagArgument ta = (TagArgument) element;
final File tagDir = ta.getDir(getProject());
if (tagDir == null) {
// The tag element is not used as a fileset,
// but specifies the tag directly.
toExecute.createArgument().setValue("-tag");
toExecute.createArgument().setValue(ta.getParameter());
} else {
// The tag element is used as a
// fileset. Parse all the files and create
// -tag arguments.
final DirectoryScanner tagDefScanner = ta.getDirectoryScanner(getProject());
for (String file : tagDefScanner.getIncludedFiles()) {
final File tagDefFile = new File(tagDir, file);
try (final BufferedReader in = new BufferedReader(new FileReader(tagDefFile))) {
in.lines().forEach(line -> {
toExecute.createArgument().setValue("-tag");
toExecute.createArgument().setValue(line);
});
} catch (final IOException ioe) {
throw new BuildException("Couldn't read tag file from " + tagDefFile.getAbsolutePath(),
ioe);
}
}
}
} else {
final ExtensionInfo tagletInfo = (ExtensionInfo) element;
toExecute.createArgument().setValue("-taglet");
toExecute.createArgument().setValue(tagletInfo.getName());
if (tagletInfo.getPath() != null) {
final Path tagletPath = tagletInfo.getPath().concatSystemClasspath("ignore");
if (!tagletPath.isEmpty()) {
toExecute.createArgument().setValue("-tagletpath");
toExecute.createArgument().setPath(tagletPath);
}
}
}
}
}
private void doDocFilesSubDirs(final Commandline toExecute) {
if (docFilesSubDirs) {
toExecute.createArgument().setValue("-docfilessubdirs");
if (excludeDocFilesSubDir != null && !excludeDocFilesSubDir.trim().isEmpty()) {
toExecute.createArgument().setValue("-excludedocfilessubdir");
toExecute.createArgument().setValue(excludeDocFilesSubDir);
}
}
}
private void doSourceAndPackageNames(
final Commandline toExecute,
final List<String> packagesToDoc,
final List<SourceFile> sourceFilesToDoc,
final boolean useExternalFile,
final File tmpList,
final BufferedWriter srcListWriter)
throws IOException {
for (final String packageName : packagesToDoc) {
if (useExternalFile) {
srcListWriter.write(packageName);
srcListWriter.newLine();
} else {
toExecute.createArgument().setValue(packageName);
}
}
for (final SourceFile sf : sourceFilesToDoc) {
final String sourceFileName = sf.getFile().getAbsolutePath();
if (useExternalFile) {
// TODO what is the following doing?
// should it run if !javadoc4 && executable != null?
if (sourceFileName.contains(" ")) {
String name = sourceFileName;
if (File.separatorChar == '\\') {
name = sourceFileName.replace(File.separatorChar, '/');
}
srcListWriter.write("\"" + name + "\"");
} else {
srcListWriter.write(sourceFileName);
}
srcListWriter.newLine();
} else {
toExecute.createArgument().setValue(sourceFileName);
}
}
}
/**
* Quote a string to place in a @ file.
* @param str the string to quote
* @return the quoted string, if there is no need to quote the string,
* return the original string.
*/
private String quoteString(final String str) {
if (!containsWhitespace(str)
&& !str.contains("'") && !str.contains("\"")) {
return str;
}
if (!str.contains("'")) {
return quoteString(str, '\'');
}
return quoteString(str, '"');
}
private boolean containsWhitespace(final String s) {
for (char c : s.toCharArray()) {
if (Character.isWhitespace(c)) {
return true;
}
}
return false;
}
private String quoteString(final String str, final char delim) {
final StringBuilder buf = new StringBuilder(str.length() * 2);
buf.append(delim);
boolean lastCharWasCR = false;
for (final char c : str.toCharArray()) {
if (c == delim) { // can't put the non-constant delim into a case
buf.append('\\').append(c);
lastCharWasCR = false;
} else {
switch (c) {
case '\\':
buf.append("\\\\");
lastCharWasCR = false;
break;
case '\r':
// insert a line continuation marker
buf.append("\\\r");
lastCharWasCR = true;
break;
case '\n':
// insert a line continuation marker unless this
// is a \r\n sequence in which case \r already has
// created the marker
if (!lastCharWasCR) {
buf.append("\\\n");
} else {
buf.append("\n");
}
lastCharWasCR = false;
break;
default:
buf.append(c);
lastCharWasCR = false;
break;
}
}
}
buf.append(delim);
return buf.toString();
}
/**
* Add the files matched by the nested source files to the Vector
* as SourceFile instances.
*
* @since 1.7
*/
private void addSourceFiles(final List<SourceFile> sf) {
for (ResourceCollection rc : nestedSourceFiles) {
if (!rc.isFilesystemOnly()) {
throw new BuildException(
"only file system based resources are supported by javadoc");
}
if (rc instanceof FileSet) {
final FileSet fs = (FileSet) rc;
if (!fs.hasPatterns() && !fs.hasSelectors()) {
final FileSet fs2 = (FileSet) fs.clone();
fs2.createInclude().setName("**/*.java");
if (includeNoSourcePackages) {
fs2.createInclude().setName("**/package.html");
}
rc = fs2;
}
}
for (final Resource r : rc) {
sf.add(new SourceFile(r.as(FileProvider.class).getFile()));
}
}
}
/**
* Add the directories matched by the nested dirsets to the Vector
* and the base directories of the dirsets to the Path. It also
* handles the packages and excludepackages attributes and
* elements.
*
* @since 1.5
*/
private void parsePackages(final List<String> pn, final Path sp) {
final Set<String> addedPackages = new HashSet<>();
final List<DirSet> dirSets = new ArrayList<>(packageSets);
// for each sourcePath entry, add a directoryset with includes
// taken from packagenames attribute and nested package
// elements and excludes taken from excludepackages attribute
// and nested excludepackage elements
if (sourcePath != null) {
final PatternSet ps = new PatternSet();
ps.setProject(getProject());
if (packageNames.isEmpty()) {
ps.createInclude().setName("**");
} else {
packageNames.stream().map(PackageName::getName)
.map(s -> s.replace('.', '/').replaceFirst("\\*$", "**"))
.forEach(pkg -> ps.createInclude().setName(pkg));
}
excludePackageNames.stream().map(PackageName::getName)
.map(s -> s.replace('.', '/').replaceFirst("\\*$", "**"))
.forEach(pkg -> ps.createExclude().setName(pkg));
for (String pathElement : sourcePath.list()) {
final File dir = new File(pathElement);
if (dir.isDirectory()) {
final DirSet ds = new DirSet();
ds.setProject(getProject());
ds.setDefaultexcludes(useDefaultExcludes);
ds.setDir(dir);
ds.createPatternSet().addConfiguredPatternset(ps);
dirSets.add(ds);
} else {
log("Skipping " + pathElement
+ " since it is no directory.", Project.MSG_WARN);
}
}
}
for (DirSet ds : dirSets) {
final File baseDir = ds.getDir(getProject());
log("scanning " + baseDir + " for packages.", Project.MSG_DEBUG);
final DirectoryScanner dsc = ds.getDirectoryScanner(getProject());
boolean containsPackages = false;
for (String dir : dsc.getIncludedDirectories()) {
// are there any java files in this directory?
final File pd = new File(baseDir, dir);
final String[] files = pd.list((directory,
name) -> name.endsWith(".java") || (includeNoSourcePackages
&& name.equals("package.html")));
if (files.length > 0) {
if (dir.isEmpty()) {
log(baseDir
+ " contains source files in the default package, you must specify them as source files not packages.",
Project.MSG_WARN);
} else {
containsPackages = true;
final String packageName =
dir.replace(File.separatorChar, '.');
if (!addedPackages.contains(packageName)) {
addedPackages.add(packageName);
pn.add(packageName);
}
}
}
}
if (containsPackages) {
// We don't need to care for duplicates here,
// Path.list does it for us.
sp.createPathElement().setLocation(baseDir);
} else {
log(baseDir + " doesn't contain any packages, dropping it.",
Project.MSG_VERBOSE);
}
}
}
private void postProcessGeneratedJavadocs() throws IOException {
if (!postProcessGeneratedJavadocs) {
return;
}
if (destDir != null && !destDir.isDirectory()) {
log("No javadoc created, no need to post-process anything",
Project.MSG_VERBOSE);
return;
}
final InputStream in = Javadoc.class
.getResourceAsStream("javadoc-frame-injections-fix.txt");
if (in == null) {
throw new FileNotFoundException(
"Missing resource 'javadoc-frame-injections-fix.txt' in classpath.");
}
final String fixData;
try {
fixData =
fixLineFeeds(FileUtils
.readFully(new InputStreamReader(in, StandardCharsets.US_ASCII)))
.trim();
} finally {
FileUtils.close(in);
}
final DirectoryScanner ds = new DirectoryScanner();
ds.setBasedir(destDir);
ds.setCaseSensitive(false);
ds.setIncludes(new String[] {
"**/index.html", "**/index.htm", "**/toc.html", "**/toc.htm"
});
ds.addDefaultExcludes();
ds.scan();
int patched = 0;
for (final String f : ds.getIncludedFiles()) {
patched += postProcess(new File(destDir, f), fixData);
}
if (patched > 0) {
log("Patched " + patched + " link injection vulnerable javadocs",
Project.MSG_INFO);
}
}
private int postProcess(final File file, final String fixData) throws IOException {
final String enc = docEncoding != null ? docEncoding
: FILE_UTILS.getDefaultEncoding();
// we load the whole file as one String (toc/index files are
// generally small, because they only contain frameset declaration):
String fileContents;
try (InputStreamReader reader =
new InputStreamReader(Files.newInputStream(file.toPath()), enc)) {
fileContents = fixLineFeeds(FileUtils.safeReadFully(reader));
}
// check if file may be vulnerable because it was not
// patched with "validURL(url)":
if (!fileContents.contains("function validURL(url) {")) {
// we need to patch the file!
final String patchedFileContents = patchContent(fileContents, fixData);
if (!patchedFileContents.equals(fileContents)) {
try (final OutputStreamWriter w =
new OutputStreamWriter(Files.newOutputStream(file.toPath()), enc)) {
w.write(patchedFileContents);
w.close();
return 1;
}
}
}
return 0;
}
private String fixLineFeeds(final String orig) {
return orig.replace("\r\n", "\n")
.replace("\n", System.lineSeparator());
}
private String patchContent(final String fileContents, final String fixData) {
// using regexes here looks like overkill
final int start = fileContents.indexOf(LOAD_FRAME);
if (start >= 0) {
return fileContents.substring(0, start) + fixData
+ fileContents.substring(start + LOAD_FRAME_LEN);
}
return fileContents;
}
private void doModuleArguments(Commandline toExecute) {
if (!moduleNames.isEmpty()) {
toExecute.createArgument().setValue("--module");
toExecute.createArgument()
.setValue(moduleNames.stream().map(PackageName::getName)
.collect(Collectors.joining(",")));
}
if (modulePath != null) {
toExecute.createArgument().setValue("--module-path");
toExecute.createArgument().setPath(modulePath);
}
if (moduleSourcePath != null) {
toExecute.createArgument().setValue("--module-source-path");
toExecute.createArgument().setPath(moduleSourcePath);
}
}
private class JavadocOutputStream extends LogOutputStream {
JavadocOutputStream(final int level) {
super(Javadoc.this, level);
}
//
// Override the logging of output in order to filter out Generating
// messages. Generating messages are set to a priority of VERBOSE
// unless they appear after what could be an informational message.
//
private String queuedLine = null;
private boolean sawWarnings = false;
@Override
protected void processLine(final String line, final int messageLevel) {
if (line.contains("warning")) {
sawWarnings = true;
}
if (messageLevel == Project.MSG_INFO
&& line.startsWith("Generating ")) {
if (queuedLine != null) {
super.processLine(queuedLine, Project.MSG_VERBOSE);
}
queuedLine = line;
} else {
if (queuedLine != null) {
if (line.startsWith("Building ")) {
super.processLine(queuedLine, Project.MSG_VERBOSE);
} else {
super.processLine(queuedLine, Project.MSG_INFO);
}
queuedLine = null;
}
super.processLine(line, messageLevel);
}
}
protected void logFlush() {
if (queuedLine != null) {
super.processLine(queuedLine, Project.MSG_VERBOSE);
queuedLine = null;
}
}
public boolean sawWarnings() {
return sawWarnings;
}
}
/**
* Convenience method to expand properties.
* @param content the string to expand
* @return the converted string
*/
protected String expand(final String content) {
return getProject().replaceProperties(content);
}
}