package org.apache.maven.plugins.javadoc;

/*
 * 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
 *
 *   http://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.
 */

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.javadoc.options.BootclasspathArtifact;
import org.apache.maven.plugins.javadoc.options.DocletArtifact;
import org.apache.maven.plugins.javadoc.options.Group;
import org.apache.maven.plugins.javadoc.options.JavadocOptions;
import org.apache.maven.plugins.javadoc.options.JavadocPathArtifact;
import org.apache.maven.plugins.javadoc.options.OfflineLink;
import org.apache.maven.plugins.javadoc.options.ResourcesArtifact;
import org.apache.maven.plugins.javadoc.options.Tag;
import org.apache.maven.plugins.javadoc.options.Taglet;
import org.apache.maven.plugins.javadoc.options.TagletArtifact;
import org.apache.maven.plugins.javadoc.options.io.xpp3.JavadocOptionsXpp3Writer;
import org.apache.maven.plugins.javadoc.resolver.JavadocBundle;
import org.apache.maven.plugins.javadoc.resolver.ResourceResolver;
import org.apache.maven.plugins.javadoc.resolver.SourceResolverConfig;
import org.apache.maven.project.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.reporting.MavenReportException;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.transfer.artifact.DefaultArtifactCoordinate;
import org.apache.maven.shared.artifact.filter.resolve.AndFilter;
import org.apache.maven.shared.artifact.filter.resolve.PatternExclusionsFilter;
import org.apache.maven.shared.artifact.filter.resolve.PatternInclusionsFilter;
import org.apache.maven.shared.artifact.filter.resolve.ScopeFilter;
import org.apache.maven.shared.artifact.filter.resolve.TransformableFilter;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult;
import org.apache.maven.shared.transfer.dependencies.DefaultDependableCoordinate;
import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;
import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolverException;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.apache.maven.wagon.PathUtils;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector;
import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
import org.codehaus.plexus.languages.java.jpms.LocationManager;
import org.codehaus.plexus.languages.java.jpms.ModuleNameSource;
import org.codehaus.plexus.languages.java.jpms.ResolvePathRequest;
import org.codehaus.plexus.languages.java.jpms.ResolvePathResult;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
import org.codehaus.plexus.languages.java.version.JavaVersion;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.xml.Xpp3Dom;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.SystemUtils.isJavaVersionAtLeast;
import static org.apache.maven.plugins.javadoc.JavadocUtil.toRelative;
import static org.apache.maven.plugins.javadoc.JavadocUtil.toList;
import static org.apache.maven.plugins.javadoc.JavadocUtil.isEmpty;
import static org.apache.maven.plugins.javadoc.JavadocUtil.isNotEmpty;

/**
 * Base class with majority of Javadoc functionalities.
 *
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
 * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html">
 *      The Java API Documentation Generator, 7</a>
 * @since 2.0
 */
public abstract class AbstractJavadocMojo
    extends AbstractMojo
{
    /**
     * Classifier used in the name of the javadoc-options XML file, and in the resources bundle
     * artifact that gets attached to the project. This one is used for non-test javadocs.
     *
     * @see #TEST_JAVADOC_RESOURCES_ATTACHMENT_CLASSIFIER
     * @since 2.7
     */
    public static final String JAVADOC_RESOURCES_ATTACHMENT_CLASSIFIER = "javadoc-resources";

    /**
     * Classifier used in the name of the javadoc-options XML file, and in the resources bundle
     * artifact that gets attached to the project. This one is used for test-javadocs.
     *
     * @see #JAVADOC_RESOURCES_ATTACHMENT_CLASSIFIER
     * @since 2.7
     */
    public static final String TEST_JAVADOC_RESOURCES_ATTACHMENT_CLASSIFIER = "test-javadoc-resources";

    /**
     * The Javadoc script file name when <code>debug</code> parameter is on, i.e. javadoc.bat or javadoc.sh
     */
    protected static final String DEBUG_JAVADOC_SCRIPT_NAME = "javadoc." + ( SystemUtils.IS_OS_WINDOWS ? "bat" : "sh" );

    /**
     * The <code>options</code> file name in the output directory when calling:
     * <code>javadoc.exe(or .sh) &#x40;options &#x40;packages | &#x40;argfile | &#x40;files</code>
     */
    protected static final String OPTIONS_FILE_NAME = "options";

    /**
     * The <code>packages</code> file name in the output directory when calling:
     * <code>javadoc.exe(or .sh) &#x40;options &#x40;packages | &#x40;argfile | &#x40;files</code>
     */
    protected static final String PACKAGES_FILE_NAME = "packages";

    /**
     * The <code>argfile</code> file name in the output directory when calling:
     * <code>javadoc.exe(or .sh) &#x40;options &#x40;packages | &#x40;argfile | &#x40;files</code>
     */
    protected static final String ARGFILE_FILE_NAME = "argfile";

    /**
     * The <code>files</code> file name in the output directory when calling:
     * <code>javadoc.exe(or .sh) &#x40;options &#x40;packages | &#x40;argfile | &#x40;files</code>
     */
    protected static final String FILES_FILE_NAME = "files";

    /**
     * The current class directory
     */
    private static final String RESOURCE_DIR = ClassUtils.getPackageName( JavadocReport.class ).replace( '.', '/' );

    /**
     * Default css file name
     */
    private static final String DEFAULT_CSS_NAME = "stylesheet.css";

    /**
     * Default location for css
     */
    private static final String RESOURCE_CSS_DIR = RESOURCE_DIR + "/css";

    private static final String PACKAGE_LIST = "package-list";
    private static final String ELEMENT_LIST = "element-list";


    /**
     * For Javadoc options appears since Java 1.4.
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.1.html#summary">
     * What's New in Javadoc 1.4</a>
     *
     * @since 2.1
     */
    private static final JavaVersion SINCE_JAVADOC_1_4 = JavaVersion.parse( "1.4" );

    /**
     * For Javadoc options appears since Java 1.4.2.
     * See <a
     * href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.2.html#commandlineoptions">
     * What's New in Javadoc 1.4.2</a>
     *
     * @since 2.1
     */
    private static final JavaVersion SINCE_JAVADOC_1_4_2 = JavaVersion.parse( "1.4.2" );

    /**
     * For Javadoc options appears since Java 5.0.
     * See <a
     * href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.5.0.html#commandlineoptions">
     * What's New in Javadoc 5.0</a>
     *
     * @since 2.1
     */
    private static final JavaVersion SINCE_JAVADOC_1_5 = JavaVersion.parse( "1.5" );

    /**
     * For Javadoc options appears since Java 6.0.
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/index.html">
     * Javadoc Technology</a>
     *
     * @since 2.4
     */
    private static final JavaVersion SINCE_JAVADOC_1_6 = JavaVersion.parse( "1.6" );

    /**
     * For Javadoc options appears since Java 8.0.
     * See <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/javadoc/index.html">
     * Javadoc Technology</a>
     *
     * @since 3.0.0
     */
    private static final JavaVersion SINCE_JAVADOC_1_8 = JavaVersion.parse( "1.8" );

    /**
     *
     */
    private static final JavaVersion JAVA_VERSION = JavaVersion.JAVA_SPECIFICATION_VERSION;

    // ----------------------------------------------------------------------
    // Mojo components
    // ----------------------------------------------------------------------

    /**
     * Archiver manager
     *
     * @since 2.5
     */
    @Component
    private ArchiverManager archiverManager;

    @Component
    private ResourceResolver resourceResolver;

    @Component
    private ArtifactResolver artifactResolver;

    @Component
    private ArtifactHandlerManager artifactHandlerManager;

    @Component
    private DependencyResolver dependencyResolver;

    /**
     * Project builder
     *
     * @since 3.0
     */
    @Component
    private ProjectBuilder mavenProjectBuilder;

    /** */
    @Component
    private ToolchainManager toolchainManager;

    final LocationManager locationManager = new LocationManager();

    // ----------------------------------------------------------------------
    // Mojo parameters
    // ----------------------------------------------------------------------

    /**
     * The current build session instance. This is used for
     * toolchain manager API calls.
     */
    @Parameter( defaultValue = "${session}", readonly = true, required = true )
    protected MavenSession session;

    /**
     * The Maven Settings.
     *
     * @since 2.3
     */
    @Parameter( defaultValue = "${settings}", readonly = true, required = true )
    private Settings settings;

    /**
     * The Maven Project Object
     */
    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    protected MavenProject project;

    @Parameter( defaultValue = "${mojoExecution}", readonly = true )
    private MojoExecution mojo;

    /**
     * Specify if the Javadoc should operate in offline mode.
     */
    @Parameter( defaultValue = "${settings.offline}", required = true, readonly = true )
    private boolean isOffline;

    /**
     * Specifies the Javadoc resources directory to be included in the Javadoc (i.e. package.html, images...).
     * <br/>
     * Could be used in addition of <code>docfilessubdirs</code> parameter.
     * <br/>
     * See <a href="#docfilessubdirs">docfilessubdirs</a>.
     *
     * @see #docfilessubdirs
     * @since 2.1
     */
    @Parameter( defaultValue = "${basedir}/src/main/javadoc" )
    private File javadocDirectory;

    /**
     * Set an additional option(s) on the command line. All input will be passed as-is to the
     * {@code @options} file. You must take care of quoting and escaping. Useful for a custom doclet.
     *
     * @since 3.0.0
     */
    @Parameter
    private String[] additionalOptions;

    /**
     * Set an additional Javadoc option(s) (i.e. JVM options) on the command line.
     * Example:
     * <pre>
     * &lt;additionalJOption&gt;-J-Xss128m&lt;/additionalJOption&gt;
     * </pre>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#J">Jflag</a>.
     * <br/>
     * See <a href="http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp">vmoptions</a>.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/net/properties.html">Networking
     * Properties</a>.
     *
     * @since 2.3
     */
    @Parameter( property = "additionalJOption" )
    private String additionalJOption;

    /**
     * Set additional JVM options for the execution of the javadoc command via the '-J' option to javadoc.
     * Example:
     * <pre>
     *     &lt;additionalJOptions&gt;
     *         &lt;additionalJOption&gt;-J-Xmx1g &lt;/additionalJOption&gt;
     *     &lt;/additionalJOptions&gt;
     * </pre>
     * @since 2.9
     */
    @Parameter
    private String[] additionalJOptions;

    /**
     * A list of artifacts containing resources which should be copied into the
     * Javadoc output directory (like stylesheets, icons, etc.).
     * <br/>
     * Example:
     * <pre>
     * &lt;resourcesArtifacts&gt;
     *   &lt;resourcesArtifact&gt;
     *     &lt;groupId&gt;external.group.id&lt;/groupId&gt;
     *     &lt;artifactId&gt;external-resources&lt;/artifactId&gt;
     *     &lt;version&gt;1.0&lt;/version&gt;
     *   &lt;/resourcesArtifact&gt;
     * &lt;/resourcesArtifacts&gt;
     * </pre>
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/ResourcesArtifact.html">Javadoc</a>.
     * <br/>
     *
     * @since 2.5
     */
    @Parameter( property = "resourcesArtifacts" )
    private ResourcesArtifact[] resourcesArtifacts;

    /**
     * The local repository where the artifacts are located.
     */
    @Parameter( property = "localRepository" )
    private ArtifactRepository localRepository;

    /**
     * The projects in the reactor for aggregation report.
     */
    @Parameter( property = "reactorProjects", readonly = true )
    private List<MavenProject> reactorProjects;

    /**
     * Set this to <code>true</code> to debug the Javadoc plugin. With this, <code>javadoc.bat(or.sh)</code>,
     * <code>options</code>, <code>@packages</code> or <code>argfile</code> files are provided in the output directory.
     * <br/>
     *
     * @since 2.1
     */
    @Parameter( property = "debug", defaultValue = "false" )
    private boolean debug;

    /**
     * Sets the absolute path of the Javadoc Tool executable to use. Since version 2.5, a mere directory specification
     * is sufficient to have the plugin use "javadoc" or "javadoc.exe" respectively from this directory.
     *
     * @since 2.3
     */
    @Parameter( property = "javadocExecutable" )
    private String javadocExecutable;

    /**
     * Version of the Javadoc Tool executable to use, ex. "1.3", "1.5".
     *
     * @since 2.3
     */
    @Parameter( property = "javadocVersion" )
    private String javadocVersion;

    /**
     * Version of the Javadoc Tool executable to use.
     */
    private JavaVersion javadocRuntimeVersion;

    /**
     * Specifies whether the Javadoc generation should be skipped.
     *
     * @since 2.5
     */
    @Parameter( property = "maven.javadoc.skip", defaultValue = "false" )
    protected boolean skip;

    /**
     * Specifies if the build will fail if there are errors during javadoc execution or not.
     *
     * @since 2.5
     */
    @Parameter( property = "maven.javadoc.failOnError", defaultValue = "true" )
    protected boolean failOnError;


    /**
     * Specifies if the build will fail if there are warning during javadoc execution or not.
     *
     * @since 3.0.1
     */
    @Parameter( property = "maven.javadoc.failOnWarnings", defaultValue = "false" )
    protected boolean failOnWarnings;

    /**
     * Specifies to use the
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#standard">
     * options provided by the Standard Doclet</a> for a custom doclet.
     * <br>
     * Example:
     * <pre>
     * &lt;docletArtifacts&gt;
     *   &lt;docletArtifact&gt;
     *     &lt;groupId&gt;com.sun.tools.doclets&lt;/groupId&gt;
     *     &lt;artifactId&gt;doccheck&lt;/artifactId&gt;
     *     &lt;version&gt;1.2b2&lt;/version&gt;
     *   &lt;/docletArtifact&gt;
     * &lt;/docletArtifacts&gt;
     * &lt;useStandardDocletOptions&gt;true&lt;/useStandardDocletOptions&gt;
     * </pre>
     *
     * @since 2.5
     */
    @Parameter( property = "useStandardDocletOptions", defaultValue = "true" )
    protected boolean useStandardDocletOptions;

    /**
     * Detect the Javadoc links for all dependencies defined in the project. The detection is based on the default
     * Maven conventions, i.e.: <code>${project.url}/apidocs</code>.
     * <br/>
     * For instance, if the project has a dependency to
     * <a href="http://commons.apache.org/lang/">Apache Commons Lang</a> i.e.:
     * <pre>
     * &lt;dependency&gt;
     *   &lt;groupId&gt;commons-lang&lt;/groupId&gt;
     *   &lt;artifactId&gt;commons-lang&lt;/artifactId&gt;
     * &lt;/dependency&gt;
     * </pre>
     * The added Javadoc <code>-link</code> parameter will be <code>http://commons.apache.org/lang/apidocs</code>.
     *
     * @see #links
     * @see #dependencyLinks
     * @since 2.6
     */
    @Parameter( property = "detectLinks", defaultValue = "false" )
    private boolean detectLinks;

    /**
     * Detect the links for all modules defined in the project.
     * <br/>
     * If {@code reactorProjects} is defined in a non-aggregator way, it generates default offline links
     * between modules based on the defined project's urls. For instance, if a parent project has two projects
     * <code>module1</code> and <code>module2</code>, the <code>-linkoffline</code> will be:
     * <br/>
     * The added Javadoc <code>-linkoffline</code> parameter for <b>module1</b> will be
     * <code>/absolute/path/to/</code><b>module2</b><code>/target/site/apidocs</code>
     * <br/>
     * The added Javadoc <code>-linkoffline</code> parameter for <b>module2</b> will be
     * <code>/absolute/path/to/</code><b>module1</b><code>/target/site/apidocs</code>
     *
     * @see #offlineLinks
     * @since 2.6
     */
    @Parameter( property = "detectOfflineLinks", defaultValue = "true" )
    private boolean detectOfflineLinks;

    /**
     * Detect the Java API link for the current build, i.e. <code>https://docs.oracle.com/javase/1.4.2/docs/api/</code>
     * for Java source 1.4.
     * <br/>
     * By default, the goal detects the Javadoc API link depending the value of the <code>source</code>
     * parameter in the <code>org.apache.maven.plugins:maven-compiler-plugin</code>
     * (defined in <code>${project.build.plugins}</code> or in <code>${project.build.pluginManagement}</code>),
     * or try to compute it from the {@code javadocExecutable} version.
     *
     * @see #links
     * @see #javaApiLinks
     * @since 2.6
     */
    @Parameter( property = "detectJavaApiLink", defaultValue = "true" )
    private boolean detectJavaApiLink;

    /**
     * Use this parameter <b>only</b> if if you want to override the default URLs.
     *
     * The key should match {@code api_x}, where {@code x} matches the Java version.
     *
     *  For example:
     *  <dl>
     *   <dt>api_1.5</dt>
     *   <dd>https://docs.oracle.com/javase/1.5.0/docs/api/</dd>
     *   <dt>api_1.8<dt>
     *   <dd>https://docs.oracle.com/javase/8/docs/api/</dd>
     *   <dt>api_9</dd>
     *   <dd>https://docs.oracle.com/javase/9/docs/api/</dd>
     * </dl>
     * @since 2.6
     */
    @Parameter( property = "javaApiLinks" )
    private Properties javaApiLinks;

    /**
     * Flag controlling content validation of <code>package-list</code> resources. If set, the content of
     * <code>package-list</code> resources will be validated.
     *
     * @since 2.8
     */
    @Parameter( property = "validateLinks", defaultValue = "false" )
    private boolean validateLinks;

    // ----------------------------------------------------------------------
    // Javadoc Options - all alphabetical
    // ----------------------------------------------------------------------

    /**
     * Specifies the paths where the boot classes reside. The <code>bootclasspath</code> can contain multiple paths
     * by separating them with a colon (<code>:</code>) or a semi-colon (<code>;</code>).
     * <br/>
     * See <a href=
     *    "https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#bootclasspath">bootclasspath</a>.
     *
     * @since 2.5
     */
    @Parameter( property = "bootclasspath" )
    private String bootclasspath;

    /**
     * Specifies the artifacts where the boot classes reside.
     * <br/>
     * See <a href=
     *   "https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#bootclasspath">bootclasspath</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;bootclasspathArtifacts&gt;
     *   &lt;bootclasspathArtifact&gt;
     *     &lt;groupId&gt;my-groupId&lt;/groupId&gt;
     *     &lt;artifactId&gt;my-artifactId&lt;/artifactId&gt;
     *     &lt;version&gt;my-version&lt;/version&gt;
     *   &lt;/bootclasspathArtifact&gt;
     * &lt;/bootclasspathArtifacts&gt;
     * </pre>
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/BootclasspathArtifact.html">Javadoc</a>.
     * <br/>
     *
     * @since 2.5
     */
    @Parameter( property = "bootclasspathArtifacts" )
    private BootclasspathArtifact[] bootclasspathArtifacts;

    /**
     * Uses the sentence break iterator to determine the end of the first sentence.
     * <br/>
     * See <a href=
     * "https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#breakiterator">breakiterator</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     * <br/>
     */
    @Parameter( property = "breakiterator", defaultValue = "false" )
    private boolean breakiterator;

    /**
     * Specifies the class file that starts the doclet used in generating the documentation.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#doclet">doclet</a>.
     */
    @Parameter( property = "doclet" )
    private String doclet;

    /**
     * Specifies the artifact containing the doclet starting class file (specified with the <code>-doclet</code>
     * option).
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#docletpath">docletpath</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;docletArtifact&gt;
     *   &lt;groupId&gt;com.sun.tools.doclets&lt;/groupId&gt;
     *   &lt;artifactId&gt;doccheck&lt;/artifactId&gt;
     *   &lt;version&gt;1.2b2&lt;/version&gt;
     * &lt;/docletArtifact&gt;
     * </pre>
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/DocletArtifact.html">Javadoc</a>.
     * <br/>
     */
    @Parameter( property = "docletArtifact" )
    private DocletArtifact docletArtifact;

    /**
     * Specifies multiple artifacts containing the path for the doclet starting class file (specified with the
     * <code>-doclet</code> option).
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#docletpath">docletpath</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;docletArtifacts&gt;
     *   &lt;docletArtifact&gt;
     *     &lt;groupId&gt;com.sun.tools.doclets&lt;/groupId&gt;
     *     &lt;artifactId&gt;doccheck&lt;/artifactId&gt;
     *     &lt;version&gt;1.2b2&lt;/version&gt;
     *   &lt;/docletArtifact&gt;
     * &lt;/docletArtifacts&gt;
     * </pre>
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/DocletArtifact.html">Javadoc</a>.
     * <br/>
     *
     * @since 2.1
     */
    @Parameter( property = "docletArtifacts" )
    private DocletArtifact[] docletArtifacts;

    /**
     * Specifies the path to the doclet starting class file (specified with the <code>-doclet</code> option) and
     * any jar files it depends on. The <code>docletPath</code> can contain multiple paths by separating them with
     * a colon (<code>:</code>) or a semi-colon (<code>;</code>).
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#docletpath">docletpath</a>.
     */
    @Parameter( property = "docletPath" )
    private String docletPath;

    /**
     * Specifies the encoding name of the source files. If not specificed, the encoding value will be the value of the
     * <code>file.encoding</code> system property.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#encoding">encoding</a>.
     * <br/>
     * <b>Note</b>: In 2.4, the default value was locked to <code>ISO-8859-1</code> to ensure reproducing build, but
     * this was reverted in 2.5.
     * <br/>
     */
    @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
    private String encoding;

    /**
     * Unconditionally excludes the specified packages and their subpackages from the list formed by
     * <code>-subpackages</code>. Multiple packages can be separated by commas (<code>,</code>), colons (<code>:</code>)
     * or semicolons (<code>;</code>).
     * <p>
     * Wildcards work as followed:
     * <ul>
     *   <li>a wildcard at the beginning should match 1 or more folders</li>
     *   <li>any other wildcard must match exactly one folder</li>
     * </ul>
     * </p>
     * <p>
     * Example:
     * <pre>
     * &lt;excludePackageNames&gt;*.internal:org.acme.exclude1.*:org.acme.exclude2&lt;/excludePackageNames&gt;
     * </pre>
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#exclude">exclude</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     * </p>
     */
    @Parameter( property = "excludePackageNames" )
    private String excludePackageNames;

    /**
     * Specifies the directories where extension classes reside. Separate directories in <code>extdirs</code> with a
     * colon (<code>:</code>) or a semi-colon (<code>;</code>).
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#extdirs">extdirs</a>.
     */
    @Parameter( property = "extdirs" )
    private String extdirs;

    /**
     * Specifies the locale that javadoc uses when generating documentation.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#locale">locale</a>.
     */
    @Parameter( property = "locale" )
    private String locale;

    /**
     * Specifies the maximum Java heap size to be used when launching the Javadoc tool.
     * JVMs refer to this property as the <code>-Xmx</code> parameter. Example: '512' or '512m'.
     * The memory unit depends on the JVM used. The units supported could be: <code>k</code>, <code>kb</code>,
     * <code>m</code>, <code>mb</code>, <code>g</code>, <code>gb</code>, <code>t</code>, <code>tb</code>.
     * If no unit specified, the default unit is <code>m</code>.
     */
    @Parameter( property = "maxmemory" )
    private String maxmemory;

    /**
     * Specifies the minimum Java heap size to be used when launching the Javadoc tool.
     * JVMs refer to this property as the <code>-Xms</code> parameter. Example: '512' or '512m'.
     * The memory unit depends on the JVM used. The units supported could be: <code>k</code>, <code>kb</code>,
     * <code>m</code>, <code>mb</code>, <code>g</code>, <code>gb</code>, <code>t</code>, <code>tb</code>.
     * If no unit specified, the default unit is <code>m</code>.
     */
    @Parameter( property = "minmemory" )
    private String minmemory;

    /**
     * This option creates documentation with the appearance and functionality of documentation generated by
     * Javadoc 1.1.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#1.1">1.1</a>.
     * <br/>
     */
    @Parameter( property = "old", defaultValue = "false" )
    private boolean old;

    /**
     * Specifies that javadoc should retrieve the text for the overview documentation from the "source" file
     * specified by path/filename and place it on the Overview page (overview-summary.html).
     * <br/>
     * <b>Note</b>: could be in conflict with &lt;nooverview/&gt;.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#overview">overview</a>.
     * <br/>
     */
    @Parameter( property = "overview", defaultValue = "${basedir}/src/main/javadoc/overview.html" )
    private File overview;

    /**
     * Shuts off non-error and non-warning messages, leaving only the warnings and errors appear, making them
     * easier to view.
     * <br/>
     * Note: was a standard doclet in Java 1.4.2 (refer to bug ID
     * <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4714350">4714350</a>).
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#quiet">quiet</a>.
     * <br/>
     * Since Java 5.0.
     * <br/>
     */
    @Parameter( property = "quiet", defaultValue = "false" )
    private boolean quiet;

    /**
     * Specifies the access level for classes and members to show in the Javadocs.
     * Possible values are:
     * <ul>
     * <li><a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#public">public</a>
     * (shows only public classes and members)</li>
     * <li><a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#protected">protected</a>
     * (shows only public and protected classes and members)</li>
     * <li><a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#package">package</a>
     * (shows all classes and members not marked private)</li>
     * <li><a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#private">private</a>
     * (shows all classes and members)</li>
     * </ul>
     * <br/>
     */
    @Parameter( property = "show", defaultValue = "protected" )
    private String show;

    /**
     * Necessary to enable javadoc to handle assertions introduced in J2SE v 1.4 source code or generics introduced in
     * J2SE v5.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#source">source</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     */
    @Parameter( property = "source", defaultValue = "${maven.compiler.source}" )
    private String source;

    /**
     * Provide source compatibility with specified release
     *
     * @since JDK 9
     * @since 3.1.0
     */
    @Parameter( defaultValue = "${maven.compiler.release}" )
    private String release;

    /**
     * Specifies the source paths where the subpackages are located. The <code>sourcepath</code> can contain
     * multiple paths by separating them with a colon (<code>:</code>) or a semi-colon (<code>;</code>).
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#sourcepath">sourcepath</a>.
     */
    @Parameter( property = "sourcepath" )
    private String sourcepath;

    /**
     * Specifies the package directory where javadoc will be executed. Multiple packages can be separated by
     * colons (<code>:</code>).
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#subpackages">subpackages</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     */
    @Parameter( property = "subpackages" )
    private String subpackages;

    /**
     * Provides more detailed messages while javadoc is running.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#verbose">verbose</a>.
     * <br/>
     */
    @Parameter( property = "verbose", defaultValue = "false" )
    private boolean verbose;

    // ----------------------------------------------------------------------
    // Standard Doclet Options - all alphabetical
    // ----------------------------------------------------------------------

    /**
     * Specifies whether or not the author text is included in the generated Javadocs.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#author">author</a>.
     * <br/>
     */
    @Parameter( property = "author", defaultValue = "true" )
    private boolean author;

    /**
     * Specifies the text to be placed at the bottom of each output file.<br/>
     * If you want to use html, you have to put it in a CDATA section, <br/>
     * e.g. <code>&lt;![CDATA[Copyright 2005, &lt;a href="http://www.mycompany.com">MyCompany, Inc.&lt;a>]]&gt;</code>
     * <br>
     * <strong>Note:<strong>If the project has the property <code>project.build.outputTimestamp</code>, its year will
     * be used as {currentYear}. This way it is possible to generate reproducible javadoc jars.
     * <br>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#bottom">bottom</a>.
     * <br>
     */
    @Parameter( property = "bottom",
                    defaultValue = "Copyright &#169; {inceptionYear}&#x2013;{currentYear} {organizationName}. "
                        + "All rights reserved." )
    private String bottom;

    /**
     * Specifies the HTML character set for this document. If not specificed, the charset value will be the value of
     * the <code>docencoding</code> parameter.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#charset">charset</a>.
     * <br/>
     */
    @Parameter( property = "charset" )
    private String charset;

    /**
     * Specifies the encoding of the generated HTML files. If not specificed, the docencoding value will be
     * <code>UTF-8</code>.
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#docencoding">docencoding</a>.
     */
    @Parameter( property = "docencoding", defaultValue = "${project.reporting.outputEncoding}" )
    private String docencoding;

    /**
     * Enables deep copying of the <code>&#42;&#42;/doc-files</code> directories and the specifc <code>resources</code>
     * directory from the <code>javadocDirectory</code> directory (for instance,
     * <code>src/main/javadoc/com/mycompany/myapp/doc-files</code> and <code>src/main/javadoc/resources</code>).
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#docfilessubdirs">
     * docfilessubdirs</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     * <br/>
     * See <a href="#javadocDirectory">javadocDirectory</a>.
     * <br/>
     *
     * @see #excludedocfilessubdir
     * @see #javadocDirectory
     */
    @Parameter( property = "docfilessubdirs", defaultValue = "false" )
    private boolean docfilessubdirs;

    /**
     * Specifies specific checks to be performed on Javadoc comments.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#BEJEFABE">doclint</a>.
     *
     * @since 3.0.0
     */
    @Parameter( property = "doclint" )
    private String doclint;

    /**
     * Specifies the title to be placed near the top of the overview summary file.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#doctitle">doctitle</a>.
     * <br/>
     */
    @Parameter( property = "doctitle", defaultValue = "${project.name} ${project.version} API" )
    private String doctitle;

    /**
     * Excludes any "doc-files" subdirectories with the given names. Multiple patterns can be excluded
     * by separating them with colons (<code>:</code>).
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#excludedocfilessubdir">
     * excludedocfilessubdir</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     *
     * @see #docfilessubdirs
     */
    @Parameter( property = "excludedocfilessubdir" )
    private String excludedocfilessubdir;

    /**
     * Specifies the footer text to be placed at the bottom of each output file.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#footer">footer</a>.
     */
    @Parameter( property = "footer" )
    private String footer;

    /**
     * Separates packages on the overview page into whatever groups you specify, one group per table. The
     * packages pattern can be any package name, or can be the start of any package name followed by an asterisk
     * (<code>*</code>) meaning "match any characters". Multiple patterns can be included in a group
     * by separating them with colons (<code>:</code>).
     * <br/>
     * Example:
     * <pre>
     * &lt;groups&gt;
     *   &lt;group&gt;
     *     &lt;title&gt;Core Packages&lt;/title&gt;
     *     &lt;!-- To includes java.lang, java.lang.ref,
     *     java.lang.reflect and only java.util
     *     (i.e. not java.util.jar) --&gt;
     *     &lt;packages&gt;java.lang*:java.util&lt;/packages&gt;
     *   &lt;/group&gt;
     *   &lt;group&gt;
     *     &lt;title&gt;Extension Packages&lt;/title&gt;
     *     &nbsp;&lt;!-- To include javax.accessibility,
     *     javax.crypto, ... (among others) --&gt;
     *     &lt;packages&gt;javax.*&lt;/packages&gt;
     *   &lt;/group&gt;
     * &lt;/groups&gt;
     * </pre>
     * <b>Note</b>: using <code>java.lang.*</code> for <code>packages</code> would omit the <code>java.lang</code>
     * package but using <code>java.lang*</code> will include it.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#group">group</a>.
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/Group.html">Javadoc</a>.
     * <br/>
     */
    @Parameter
    private Group[] groups;

    /**
     * Specifies the header text to be placed at the top of each output file.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#header">header</a>.
     */
    @Parameter( property = "header" )
    private String header;

    /**
     * Specifies the path of an alternate help file path\filename that the HELP link in the top and bottom
     * navigation bars link to.
     * <br/>
     * <b>Note</b>: could be in conflict with &lt;nohelp/&gt;.
     * <br/>
     * The <code>helpfile</code> could be an absolute File path.
     * <br/>
     * Since 2.6, it could be also be a path from a resource in the current project source directories
     * (i.e. <code>src/main/java</code>, <code>src/main/resources</code> or <code>src/main/javadoc</code>)
     * or from a resource in the Javadoc plugin dependencies, for instance:
     * <pre>
     * &lt;helpfile&gt;path/to/your/resource/yourhelp-doc.html&lt;/helpfile&gt;
     * </pre>
     * Where <code>path/to/your/resource/yourhelp-doc.html</code> could be in <code>src/main/javadoc</code>.
     * <pre>
     * &lt;build&gt;
     *   &lt;plugins&gt;
     *     &lt;plugin&gt;
     *       &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
     *       &lt;artifactId&gt;maven-javadoc-plugin&lt;/artifactId&gt;
     *       &lt;configuration&gt;
     *         &lt;helpfile&gt;path/to/your/resource/yourhelp-doc.html&lt;/helpfile&gt;
     *         ...
     *       &lt;/configuration&gt;
     *       &lt;dependencies&gt;
     *         &lt;dependency&gt;
     *           &lt;groupId&gt;groupId&lt;/groupId&gt;
     *           &lt;artifactId&gt;artifactId&lt;/artifactId&gt;
     *           &lt;version&gt;version&lt;/version&gt;
     *         &lt;/dependency&gt;
     *       &lt;/dependencies&gt;
     *     &lt;/plugin&gt;
     *     ...
     *   &lt;plugins&gt;
     * &lt;/build&gt;
     * </pre>
     * Where <code>path/to/your/resource/yourhelp-doc.html</code> is defined in the
     * <code>groupId:artifactId:version</code> javadoc plugin dependency.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#helpfile">helpfile</a>.
     */
    @Parameter( property = "helpfile" )
    private String helpfile;

    /**
     * Adds HTML meta keyword tags to the generated file for each class.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#keywords">keywords</a>.
     * <br/>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.2.html#commandlineoptions">
     * Java 1.4.2</a>.
     * <br/>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.5.0.html#commandlineoptions">
     * Java 5.0</a>.
     * <br/>
     *
     * @since 2.1
     */
    @Parameter( property = "keywords", defaultValue = "false" )
    private boolean keywords;

    /**
     * Creates links to existing javadoc-generated documentation of external referenced classes.
     * <br>
     * <b>Notes</b>:
     * <ol>
     * <li>only used if {@code isOffline} is set to <code>false</code>.</li>
     * <li>all given links should have a fetchable <code>/package-list</code> file. For instance:
     * <pre>
     * &lt;links&gt;
     *   &lt;link&gt;https://docs.oracle.com/javase/1.4.2/docs/api&lt;/link&gt;
     * &lt;links&gt;
     * </pre>
     * will be used because <code>https://docs.oracle.com/javase/1.4.2/docs/api/package-list</code> exists.</li>
     * <li>if {@code detectLinks} is defined, the links between the project dependencies are
     * automatically added.</li>
     * <li>if {@code detectJavaApiLink} is defined, a Java API link, based on the Java version of the
     * project's sources, will be added automatically.</li>
     * </ol>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#link">link</a>.
     */
    @Parameter( property = "links" )
    protected ArrayList<String> links;


    /**
     * Redefine the apidoc URL for specific dependencies when using {@code detectLinks}.
     * Useful if the dependency wasn't build with Maven or when the apidocs have been moved.
     * <pre>
     * &lt;dependencyLinks&gt;
     *   &lt;dependencyLink&gt;
     *     &lt;groupId&gt;groupId&lt;/groupId&gt;
     *     &lt;artifactId&gt;artifactId&lt;/artifactId&gt;
     *     &lt;classifier&gt;classifier&lt;/classifier&gt; &lt;!-- optional --&gt;
     *     &lt;url&gt;version&lt;/url&gt;
     *   &lt;/dependencyLink&gt;
     * &lt;/dependencyLinks&gt;
     * </pre>
     *
     * @see #detectLinks
     * @since 3.3.0
     */
    @Parameter
    private List<DependencyLink> dependencyLinks = new ArrayList<>();

    /**
     * Creates an HTML version of each source file (with line numbers) and adds links to them from the standard
     * HTML documentation.
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#linksource">linksource</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     * <br/>
     */
    @Parameter( property = "linksource", defaultValue = "false" )
    private boolean linksource;

    /**
     * Suppress the entire comment body, including the main description and all tags, generating only declarations.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#nocomment">nocomment</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     * <br/>
     */
    @Parameter( property = "nocomment", defaultValue = "false" )
    private boolean nocomment;

    /**
     * Prevents the generation of any deprecated API at all in the documentation.
     * <br/>
     * See
     * <a
     * href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#nodeprecated">nodeprecated</a>.
     * <br/>
     */
    @Parameter( property = "nodeprecated", defaultValue = "false" )
    private boolean nodeprecated;

    /**
     * Prevents the generation of the file containing the list of deprecated APIs (deprecated-list.html) and the
     * link in the navigation bar to that page.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#nodeprecatedlist">
     * nodeprecatedlist</a>.
     * <br/>
     */
    @Parameter( property = "nodeprecatedlist", defaultValue = "false" )
    private boolean nodeprecatedlist;

    /**
     * Omits the HELP link in the navigation bars at the top and bottom of each page of output.
     * <br/>
     * <b>Note</b>: could be in conflict with &lt;helpfile/&gt;.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#nohelp">nohelp</a>.
     * <br/>
     */
    @Parameter( property = "nohelp", defaultValue = "false" )
    private boolean nohelp;

    /**
     * Omits the index from the generated docs.
     * <br/>
     * <b>Note</b>: could be in conflict with &lt;splitindex/&gt;.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#noindex">noindex</a>.
     * <br/>
     */
    @Parameter( property = "noindex", defaultValue = "false" )
    private boolean noindex;

    /**
     * Omits the navigation bar from the generated docs.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#nonavbar">nonavbar</a>.
     * <br/>
     */
    @Parameter( property = "nonavbar", defaultValue = "false" )
    private boolean nonavbar;

    /**
     * Omits the entire overview page from the generated docs.
     * <br/>
     * <b>Note</b>: could be in conflict with &lt;overview/&gt;.
     * <br/>
     * Standard Doclet undocumented option.
     * <br/>
     *
     * @since 2.4
     */
    @Parameter( property = "nooverview", defaultValue = "false" )
    private boolean nooverview;

    /**
     * Omits qualifying package name from ahead of class names in output.
     * Example:
     * <pre>
     * &lt;noqualifier&gt;all&lt;/noqualifier&gt;
     * or
     * &lt;noqualifier&gt;packagename1:packagename2&lt;/noqualifier&gt;
     * </pre>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#noqualifier">noqualifier</a>.
     * <br/>
     * Since <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java
     * 1.4</a>.
     */
    @Parameter( property = "noqualifier" )
    private String noqualifier;

    /**
     * Omits from the generated docs the "Since" sections associated with the since tags.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#nosince">nosince</a>.
     * <br/>
     */
    @Parameter( property = "nosince", defaultValue = "false" )
    private boolean nosince;

    /**
     * Suppresses the timestamp, which is hidden in an HTML comment in the generated HTML near the top of each page.
     * <br><br>
     * <strong>Note:</strong> If the project has the property <code>project.build.outputTimestamp</code>, the value
     * will be overwritten to true. This way it is possible to generate reproducible javadoc jars.
     * <br><br>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#notimestamp">notimestamp</a>.
     * <br>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.5.0.html#commandlineoptions">
     * Java 5.0</a>.
     *
     * @since 2.1
     */
    @Parameter( property = "notimestamp", defaultValue = "false" )
    private boolean notimestamp;

    /**
     * Omits the class/interface hierarchy pages from the generated docs.
     * <br>
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#notree">notree</a> option
     */
    @Parameter( property = "notree", defaultValue = "false" )
    private boolean notree;

    /**
     * This option is a variation of <code>-link</code>; they both create links to javadoc-generated documentation
     * for external referenced classes.
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#linkoffline">linkoffline</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;offlineLinks&gt;
     *   &lt;offlineLink&gt;
     *     &lt;url&gt;https://docs.oracle.com/javase/1.5.0/docs/api/&lt;/url&gt;
     *     &lt;location&gt;../javadoc/jdk-5.0/&lt;/location&gt;
     *   &lt;/offlineLink&gt;
     * &lt;/offlineLinks&gt;
     * </pre>
     * <br/>
     * <b>Note</b>: if {@code detectOfflineLinks} is defined, the offline links between the project modules are
     * automatically added if the goal is calling in a non-aggregator way.
     * <br>
     * @see <a href="./apidocs/org/apache/maven/plugins/javadoc/options/OfflineLink.html">Javadoc</a>.
     */
    @Parameter( property = "offlineLinks" )
    private OfflineLink[] offlineLinks;

    /**
     * Specifies the destination directory where javadoc saves the generated HTML files.
     * <br>
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#d">javadoc d</a>
     */
    @Parameter( property = "destDir", alias = "destDir", defaultValue = "${project.build.directory}/apidocs",
                    required = true )
    protected File outputDirectory;

    /**
     * Specify the text for upper left frame.
     * <br/>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.2.html#commandlineoptions">
     * Java 1.4.2</a>.
     *
     * @since 2.1
     */
    @Parameter( property = "packagesheader" )
    private String packagesheader;

    /**
     * Generates compile-time warnings for missing serial tags.
     * <br/>
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#serialwarn">serialwarn</a> option
     */
    @Parameter( property = "serialwarn", defaultValue = "false" )
    private boolean serialwarn;

    /**
     * Specify the number of spaces each tab takes up in the source. If no tab is used in source, the default
     * space is used.
     * <br/>
     * Note: was <code>linksourcetab</code> in Java 1.4.2 (refer to bug ID
     * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4788919">4788919</a>).
     * <br/>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.2.html#commandlineoptions">
     * 1.4.2</a>.
     * <br/>
     * Since Java 5.0.
     *
     * @since 2.1
     */
    @Parameter( property = "sourcetab", alias = "linksourcetab" )
    private int sourcetab;

    /**
     * Splits the index file into multiple files, alphabetically, one file per letter, plus a file for any index
     * entries that start with non-alphabetical characters.
     * <br/>
     * <b>Note</b>: could be in conflict with &lt;noindex/&gt;.
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#splitindex">splitindex</a>.
     * <br/>
     */
    @Parameter( property = "splitindex", defaultValue = "false" )
    private boolean splitindex;

    /**
     * Specifies whether the stylesheet to be used is the <code>maven</code>'s javadoc stylesheet or
     * <code>java</code>'s default stylesheet when a <i>stylesheetfile</i> parameter is not specified.
     * <br/>
     * Possible values: <code>maven<code> or <code>java</code>.
     * <br/>
     */
    @Parameter( property = "stylesheet", defaultValue = "java" )
    private String stylesheet;

    /**
     * Specifies the path of an alternate HTML stylesheet file.
     * <br/>
     * The <code>stylesheetfile</code> could be an absolute File path.
     * <br/>
     * Since 2.6, it could be also be a path from a resource in the current project source directories
     * (i.e. <code>src/main/java</code>, <code>src/main/resources</code> or <code>src/main/javadoc</code>)
     * or from a resource in the Javadoc plugin dependencies, for instance:
     * <pre>
     * &lt;stylesheetfile&gt;path/to/your/resource/yourstylesheet.css&lt;/stylesheetfile&gt;
     * </pre>
     * Where <code>path/to/your/resource/yourstylesheet.css</code> could be in <code>src/main/javadoc</code>.
     * <pre>
     * &lt;build&gt;
     *   &lt;plugins&gt;
     *     &lt;plugin&gt;
     *       &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
     *       &lt;artifactId&gt;maven-javadoc-plugin&lt;/artifactId&gt;
     *       &lt;configuration&gt;
     *         &lt;stylesheetfile&gt;path/to/your/resource/yourstylesheet.css&lt;/stylesheetfile&gt;
     *         ...
     *       &lt;/configuration&gt;
     *       &lt;dependencies&gt;
     *         &lt;dependency&gt;
     *           &lt;groupId&gt;groupId&lt;/groupId&gt;
     *           &lt;artifactId&gt;artifactId&lt;/artifactId&gt;
     *           &lt;version&gt;version&lt;/version&gt;
     *         &lt;/dependency&gt;
     *       &lt;/dependencies&gt;
     *     &lt;/plugin&gt;
     *     ...
     *   &lt;plugins&gt;
     * &lt;/build&gt;
     * </pre>
     * Where <code>path/to/your/resource/yourstylesheet.css</code> is defined in the
     * <code>groupId:artifactId:version</code> javadoc plugin dependency.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#stylesheetfile">
     * stylesheetfile</a>.
     */
    @Parameter( property = "stylesheetfile" )
    private String stylesheetfile;

    /**
     * Specifies the path of an additional HTML stylesheet file relative to the {@code javadocDirectory}
     * Example:
     * <pre>
     *     &lt;addStylesheets&gt;
     *         &lt;resources/addstylesheet.css&lt;/addStylesheet&gt;
     *     &lt;/addStylesheets&gt;
     * </pre>
     * @since 3.3.0
     */
    @Parameter
    private String[] addStylesheets;

    /**
     * Specifies the class file that starts the taglet used in generating the documentation for that tag.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#taglet">taglet</a>.
     * <br/>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java 1.4</a>.
     */
    @Parameter( property = "taglet" )
    private String taglet;

    /**
     * Specifies the Taglet artifact containing the taglet class files (.class).
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#tagletpath">tagletpath</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;taglets&gt;
     *   &lt;taglet&gt;
     *     &lt;tagletClass&gt;com.sun.tools.doclets.ToDoTaglet&lt;/tagletClass&gt;
     *   &lt;/taglet&gt;
     *   &lt;taglet&gt;
     *     &lt;tagletClass&gt;package.to.AnotherTagletClass&lt;/tagletClass&gt;
     *   &lt;/taglet&gt;
     *   ...
     * &lt;/taglets&gt;
     * &lt;tagletArtifact&gt;
     *   &lt;groupId&gt;group-Taglet&lt;/groupId&gt;
     *   &lt;artifactId&gt;artifact-Taglet&lt;/artifactId&gt;
     *   &lt;version&gt;version-Taglet&lt;/version&gt;
     * &lt;/tagletArtifact&gt;
     * </pre>
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/TagletArtifact.html">Javadoc</a>.
     * <br/>
     *
     * @since 2.1
     */
    @Parameter( property = "tagletArtifact" )
    private TagletArtifact tagletArtifact;

    /**
     * Specifies several Taglet artifacts containing the taglet class files (.class). These taglets class names will be
     * auto-detect and so no need to specify them.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#taglet">taglet</a>.
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#tagletpath">tagletpath</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;tagletArtifacts&gt;
     *   &lt;tagletArtifact&gt;
     *     &lt;groupId&gt;group-Taglet&lt;/groupId&gt;
     *     &lt;artifactId&gt;artifact-Taglet&lt;/artifactId&gt;
     *     &lt;version&gt;version-Taglet&lt;/version&gt;
     *   &lt;/tagletArtifact&gt;
     *   ...
     * &lt;/tagletArtifacts&gt;
     * </pre>
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/TagletArtifact.html">Javadoc</a>.
     * <br/>
     *
     * @since 2.5
     */
    @Parameter( property = "tagletArtifacts" )
    private TagletArtifact[] tagletArtifacts;

    /**
     * Specifies the search paths for finding taglet class files (.class). The <code>tagletpath</code> can contain
     * multiple paths by separating them with a colon (<code>:</code>) or a semi-colon (<code>;</code>).
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#tagletpath">tagletpath</a>.
     * <br/>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java 1.4</a>.
     */
    @Parameter( property = "tagletpath" )
    private String tagletpath;

    /**
     * Enables the Javadoc tool to interpret multiple taglets.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#taglet">taglet</a>.
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#tagletpath">tagletpath</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;taglets&gt;
     *   &lt;taglet&gt;
     *     &lt;tagletClass&gt;com.sun.tools.doclets.ToDoTaglet&lt;/tagletClass&gt;
     *     &lt;!--&lt;tagletpath&gt;/home/taglets&lt;/tagletpath&gt;--&gt;
     *     &lt;tagletArtifact&gt;
     *       &lt;groupId&gt;group-Taglet&lt;/groupId&gt;
     *       &lt;artifactId&gt;artifact-Taglet&lt;/artifactId&gt;
     *       &lt;version&gt;version-Taglet&lt;/version&gt;
     *     &lt;/tagletArtifact&gt;
     *   &lt;/taglet&gt;
     * &lt;/taglets&gt;
     * </pre>
     * <br/>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/Taglet.html">Javadoc</a>.
     * <br/>
     *
     * @since 2.1
     */
    @Parameter( property = "taglets" )
    private Taglet[] taglets;

    /**
     * Enables the Javadoc tool to interpret a simple, one-argument custom block tag tagname in doc comments.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#tag">tag</a>.
     * <br/>
     * Since
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#summary">Java 1.4</a>.
     * <br/>
     * Example:
     * <pre>
     * &lt;tags&gt;
     *   &lt;tag&gt;
     *     &lt;name&gt;todo&lt;/name&gt;
     *     &lt;placement&gt;a&lt;/placement&gt;
     *     &lt;head&gt;To Do:&lt;/head&gt;
     *   &lt;/tag&gt;
     * &lt;/tags&gt;
     * </pre>
     * <b>Note</b>: the placement should be a combinaison of Xaoptcmf letters:
     * <ul>
     * <li><b><code>X</code></b> (disable tag)</li>
     * <li><b><code>a</code></b> (all)</li>
     * <li><b><code>o</code></b> (overview)</li>
     * <li><b><code>p</code></b> (packages)</li>
     * <li><b><code>t</code></b> (types, that is classes and interfaces)</li>
     * <li><b><code>c</code></b> (constructors)</li>
     * <li><b><code>m</code></b> (methods)</li>
     * <li><b><code>f</code></b> (fields)</li>
     * </ul>
     * See <a href="./apidocs/org/apache/maven/plugins/javadoc/options/Tag.html">Javadoc</a>.
     * <br/>
     */
    @Parameter( property = "tags" )
    private Tag[] tags;

    /**
     * Specifies the top text to be placed at the top of each output file.
     * <br/>
     * See <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6227616">6227616</a>.
     * <br/>
     * Since Java 6.0
     *
     * @since 2.4
     */
    @Parameter( property = "top" )
    private String top;

    /**
     * Includes one "Use" page for each documented class and package.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#use">use</a>.
     * <br/>
     */
    @Parameter( property = "use", defaultValue = "true" )
    private boolean use;

    /**
     * Includes the version text in the generated docs.
     * <br/>
     * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#version">version</a>.
     * <br/>
     */
    @Parameter( property = "version", defaultValue = "true" )
    private boolean version;

    /**
     * Specifies the title to be placed in the HTML title tag.
     * <br/>
     * See
     * <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#windowtitle">windowtitle</a>.
     * <br/>
     */
    @Parameter( property = "windowtitle", defaultValue = "${project.name} ${project.version} API" )
    private String windowtitle;

    /**
     * Whether dependency -sources jars should be resolved and included as source paths for javadoc generation.
     * This is useful when creating javadocs for a distribution project.
     *
     * @since 2.7
     */
    @Parameter( defaultValue = "false" )
    private boolean includeDependencySources;

    /**
     * Directory where unpacked project sources / test-sources should be cached.
     *
     * @see #includeDependencySources
     * @since 2.7
     */
    @Parameter( defaultValue = "${project.build.directory}/distro-javadoc-sources" )
    private File sourceDependencyCacheDir;

    /**
     * Whether to include transitive dependencies in the list of dependency -sources jars to include in this javadoc
     * run.
     *
     * @see #includeDependencySources
     * @since 2.7
     * @deprecated if these sources depend on transitive dependencies, those dependencies should be added to the pom as
     *             direct dependencies
     */
    @Parameter( defaultValue = "false" )
    @Deprecated
    private boolean includeTransitiveDependencySources;

    /**
     * List of included dependency-source patterns. Example: <code>org.apache.maven:*</code>
     *
     * @see #includeDependencySources
     * @since 2.7
     */
    @Parameter
    private List<String> dependencySourceIncludes;

    /**
     * List of excluded dependency-source patterns. Example: <code>org.apache.maven.shared:*</code>
     *
     * @see #includeDependencySources
     * @since 2.7
     */
    @Parameter
    private List<String> dependencySourceExcludes;

    /**
     * Directory into which assembled {@link JavadocOptions} instances will be written before they
     * are added to javadoc resources bundles.
     *
     * @since 2.7
     */
    @Parameter( defaultValue = "${project.build.directory}/javadoc-bundle-options", readonly = true )
    private File javadocOptionsDir;

    /**
     * Transient variable to allow lazy-resolution of javadoc bundles from dependencies, so they can
     * be used at various points in the javadoc generation process.
     *
     * @since 2.7
     */
    private transient List<JavadocBundle> dependencyJavadocBundles;

    /**
     * Capability to add additional dependencies to the javadoc classpath.
     * Example:
     * <pre>
     * &lt;additionalDependencies&gt;
     *   &lt;additionalDependency&gt;
     *     &lt;groupId&gt;geronimo-spec&lt;/groupId&gt;
     *     &lt;artifactId&gt;geronimo-spec-jta&lt;/artifactId&gt;
     *     &lt;version&gt;1.0.1B-rc4&lt;/version&gt;
     *   &lt;/additionalDependency&gt;
     * &lt;/additionalDependencies&gt;
     * </pre>
     *
     * @since 2.8.1
     */
    @Parameter
    private List<AdditionalDependency> additionalDependencies;

    /**
     * Include filters on the source files. Default is **\/\*.java.
     * These are ignored if you specify subpackages or subpackage excludes.
     *
     * @since 2.9
     */
    @Parameter
    private List<String> sourceFileIncludes;

    /**
     * exclude filters on the source files.
     * These are ignored if you specify subpackages or subpackage excludes.
     *
     * @since 2.9
     */
    @Parameter
    private List<String> sourceFileExcludes;

    /**
     * To apply the security fix on generated javadoc see http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-1571
     * @since 2.9.1
     */
    @Parameter( defaultValue = "true", property = "maven.javadoc.applyJavadocSecurityFix" )
    private boolean applyJavadocSecurityFix = true;

    /**
     * <p>
     * Allow for configuration of the javadoc tool via maven toolchains.
     * This overrules the toolchain selected by the maven-toolchain-plugin.
     * </p>
     *
     * <p>Examples:</p>
     * (see <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html">
     *     Guide to Toolchains</a> for more info)
     *
     * <pre>
     * {@code
     *    <configuration>
     *        ...
     *        <jdkToolchain>
     *            <version>11</version>
     *        </jdkToolchain>
     *    </configuration>
     *
     *    <configuration>
     *        ...
     *        <jdkToolchain>
     *            <version>1.8</version>
     *            <vendor>zulu</vendor>
     *        </jdkToolchain>
     *    </configuration>
     *    }
     * </pre>
     *
     * <strong>note:</strong> requires at least Maven 3.3.1
     *
     * @since 3.0.0
     */
    @Parameter
    private Map<String, String> jdkToolchain;

    /**
     * <p>
     * Location of the file used to store the state of the previous javadoc run.
     * This is used to skip the generation if nothing has changed.
     * </p>
     *
     * @since 3.2.0
     */
    @Parameter( property = "staleDataPath",
            defaultValue = "${project.build.directory}/maven-javadoc-plugin-stale-data.txt" )
    private File staleDataPath;


    /**
     * <p>
     * Comma separated list of modules (artifactId) to not add in aggregated javadoc
     * </p>
     *
     * @since 3.2.0
     */
    @Parameter( property = "maven.javadoc.skippedModules" )
    private String skippedModules;

    /**
     * Timestamp for reproducible output archive entries, either formatted as ISO 8601
     * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
     * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
     *
     * @since 3.2.0
     */
    @Parameter( defaultValue = "${project.build.outputTimestamp}" )
    protected String outputTimestamp;

    // ----------------------------------------------------------------------
    // protected methods
    // ----------------------------------------------------------------------

    /**
     * Indicates whether this goal is flagged with <code>@aggregator</code>.
     *
     * @return <code>true</code> if the goal is designed as an aggregator, <code>false</code> otherwise.
     * @see AggregatorJavadocReport
     * @see AggregatorTestJavadocReport
     */
    protected boolean isAggregator()
    {
        return false;
    }

    /**
     * Indicates whether this goal generates documentation for the <code>Java Test code</code>.
     *
     * @return <code>true</code> if the goal generates Test Javadocs, <code>false</code> otherwise.
     */
    protected boolean isTest()
    {
        return false;
    }

    /**
     * @return the output directory
     */
    protected String getOutputDirectory()
    {
        return outputDirectory.getAbsoluteFile().toString();
    }

    protected MavenProject getProject()
    {
        return project;
    }

    /**
     * @param p not null maven project
     * @return the list of directories where compiled classes are placed for the given project. These dirs are
     *         added in the javadoc classpath.
     */
    protected List<File> getProjectBuildOutputDirs( MavenProject p )
    {
        if ( StringUtils.isEmpty( p.getBuild().getOutputDirectory() ) )
        {
            return Collections.emptyList();
        }

        return Collections.singletonList( new File( p.getBuild().getOutputDirectory() ) );
    }

    /**
     * Either returns the attached artifact file or outputDirectory
     *
     * @param project
     * @return
     */
    protected File getClassesFile( MavenProject project )
    {
        if ( !isAggregator() && isTest() )
        {
            return null;
        }

        if ( project.getArtifact() != null && project.getArtifact().getFile() != null )
        {
            File artifactFile = project.getArtifact().getFile();
            if ( artifactFile.isDirectory() || artifactFile.getName().endsWith( ".jar" ) )
            {
                return artifactFile;
            }
        }
        else if ( project.getExecutionProject() != null
                        && project.getExecutionProject().getArtifact() != null
                        && project.getExecutionProject().getArtifact().getFile() != null )
        {
            File artifactFile = project.getExecutionProject().getArtifact().getFile();
            if ( artifactFile.isDirectory() || artifactFile.getName().endsWith( ".jar" ) )
            {
                return artifactFile;
            }
        }

        if ( project.getBuild().getOutputDirectory() != null )
        {
            return new File( project.getBuild().getOutputDirectory() );
        }
        else
        {
            return null;
        }
    }

    /**
     * @param p not null maven project
     * @return the list of source paths for the given project
     */
    protected List<String> getProjectSourceRoots( MavenProject p )
    {
        if ( "pom".equals( p.getPackaging().toLowerCase() ) )
        {
            return Collections.emptyList();
        }

        return ( p.getCompileSourceRoots() == null
            ? Collections.<String>emptyList()
            : new LinkedList<>( p.getCompileSourceRoots() ) );
    }

    /**
     * @param p not null maven project
     * @return the list of source paths for the execution project of the given project
     */
    protected List<String> getExecutionProjectSourceRoots( MavenProject p )
    {
        if ( "pom".equals( p.getExecutionProject().getPackaging().toLowerCase() ) )
        {
            return Collections.emptyList();
        }

        return ( p.getExecutionProject().getCompileSourceRoots() == null
            ? Collections.<String>emptyList()
            : new LinkedList<>( p.getExecutionProject().getCompileSourceRoots() ) );
    }

    /**
     * @return the current javadoc directory
     */
    protected File getJavadocDirectory()
    {
        return javadocDirectory;
    }

    /**
     * @return the doclint specific checks configuration
     */
    protected String getDoclint()
    {
        return doclint;
    }

    /**
     * @return the title to be placed near the top of the overview summary file
     */
    protected String getDoctitle()
    {
        return doctitle;
    }

    /**
     * @return the overview documentation file from the user parameter or from the <code>javadocdirectory</code>
     */
    protected File getOverview()
    {
        return overview;
    }

    /**
     * @return the title to be placed in the HTML title tag
     */
    protected String getWindowtitle()
    {
        return windowtitle;
    }

    /**
     * @return the charset attribute or the value of {@link #getDocencoding()} if <code>null</code>.
     */
    private String getCharset()
    {
        return ( StringUtils.isEmpty( charset ) ) ? getDocencoding() : charset;
    }

    /**
     * @return the docencoding attribute or <code>UTF-8</code> if <code>null</code>.
     */
    private String getDocencoding()
    {
        return ( StringUtils.isEmpty( docencoding ) ) ? ReaderFactory.UTF_8 : docencoding;
    }

    /**
     * @return the encoding attribute or the value of <code>file.encoding</code> system property if <code>null</code>.
     */
    private String getEncoding()
    {
        return ( StringUtils.isEmpty( encoding ) ) ? ReaderFactory.FILE_ENCODING : encoding;
    }

    @Override
    public void execute()
        throws MojoExecutionException, MojoFailureException
    {
        verifyRemovedParameter( "aggregator" );
        verifyRemovedParameter( "proxyHost" );
        verifyRemovedParameter( "proxyPort" );
        verifyReplacedParameter( "additionalparam", "additionalOptions" );

        doExecute();
    }

    abstract void doExecute() throws MojoExecutionException, MojoFailureException;

    protected final void verifyRemovedParameter( String paramName )
    {
        Xpp3Dom configDom = mojo.getConfiguration();
        if ( configDom != null )
        {
            if ( configDom.getChild( paramName ) != null )
            {
                throw new IllegalArgumentException( "parameter '" + paramName
                    + "' has been removed from the plugin, please verify documentation." );
            }
        }
    }

    private void verifyReplacedParameter( String oldParamName, String newParamNew )
    {
        Xpp3Dom configDom = mojo.getConfiguration();
        if ( configDom != null )
        {
            if ( configDom.getChild( oldParamName ) != null )
            {
                throw new IllegalArgumentException( "parameter '" + oldParamName
                    + "' has been replaced with " + newParamNew + ", please verify documentation." );
            }
        }
    }

    /**
     * The <a href="package-summary.html">package documentation</a> details the
     * Javadoc Options used by this Plugin.
     *
     * @param unusedLocale the wanted locale (actually unused).
     * @throws MavenReportException if any
     */
    protected void executeReport( Locale unusedLocale )
        throws MavenReportException
    {
        if ( skip )
        {
            getLog().info( "Skipping javadoc generation" );
            return;
        }

        if ( getLog().isDebugEnabled() )
        {
            this.debug = true;
        }

        // NOTE: Always generate this file, to allow javadocs from modules to be aggregated via
        // useDependencySources in a distro module build.
        try
        {
            buildJavadocOptions();
        }
        catch ( IOException e )
        {
            throw new MavenReportException( "Failed to generate javadoc options file: " + e.getMessage(), e );
        }

        Collection<JavadocModule> sourcePaths = getSourcePaths();

        Collection<Path> collectedSourcePaths = sourcePaths.stream()
                                                           .flatMap( e -> e.getSourcePaths().stream() )
                                                           .collect( Collectors.toList() );

        Map<Path, Collection<String>> files = getFiles( collectedSourcePaths );
        if ( !canGenerateReport( files ) )
        {
            return;
        }

        // ----------------------------------------------------------------------
        // Find the javadoc executable and version
        // ----------------------------------------------------------------------

        String jExecutable;
        try
        {
            jExecutable = getJavadocExecutable();
        }
        catch ( IOException e )
        {
            throw new MavenReportException( "Unable to find javadoc command: " + e.getMessage(), e );
        }
        setFJavadocVersion( new File( jExecutable ) );

        Collection<String> packageNames;
        if ( javadocRuntimeVersion.isAtLeast( "9" ) )
        {
            packageNames = getPackageNamesRespectingJavaModules( sourcePaths );
        }
        else
        {
            packageNames = getPackageNames( files );
        }

        // ----------------------------------------------------------------------
        // Javadoc output directory as File
        // ----------------------------------------------------------------------

        File javadocOutputDirectory = new File( getOutputDirectory() );
        if ( javadocOutputDirectory.exists() && !javadocOutputDirectory.isDirectory() )
        {
            throw new MavenReportException( "IOException: " + getOutputDirectory() + " is not a directory." );
        }
        if ( javadocOutputDirectory.exists() && !javadocOutputDirectory.canWrite() )
        {
            throw new MavenReportException( "IOException: " + getOutputDirectory() + " is not writable." );
        }
        javadocOutputDirectory.mkdirs();

        // ----------------------------------------------------------------------
        // Copy all resources
        // ----------------------------------------------------------------------

        copyAllResources( javadocOutputDirectory );

        // ----------------------------------------------------------------------
        // Create command line for Javadoc
        // ----------------------------------------------------------------------

        Commandline cmd = new Commandline();
        cmd.getShell().setQuotedArgumentsEnabled( false ); // for Javadoc JVM args
        cmd.setWorkingDirectory( javadocOutputDirectory.getAbsolutePath() );
        cmd.setExecutable( jExecutable );

        // ----------------------------------------------------------------------
        // Wrap Javadoc JVM args
        // ----------------------------------------------------------------------

        addMemoryArg( cmd, "-Xmx", this.maxmemory );
        addMemoryArg( cmd, "-Xms", this.minmemory );
        addProxyArg( cmd );

        if ( StringUtils.isNotEmpty( additionalJOption ) )
        {
            cmd.createArg().setValue( additionalJOption );
        }

        if ( additionalJOptions != null && additionalJOptions.length != 0 )
        {
            for ( String jo : additionalJOptions )
            {
                cmd.createArg().setValue( jo );
            }
        }

        // ----------------------------------------------------------------------
        // Wrap Standard doclet Options
        // ----------------------------------------------------------------------
        List<String> standardDocletArguments = new ArrayList<>();

        Set<OfflineLink> offlineLinks;
        if ( StringUtils.isEmpty( doclet ) || useStandardDocletOptions )
        {
            offlineLinks = getLinkofflines();
            addStandardDocletOptions( javadocOutputDirectory, standardDocletArguments, offlineLinks );
        }
        else
        {
            offlineLinks = Collections.emptySet();
        }

        // ----------------------------------------------------------------------
        // Wrap Javadoc options
        // ----------------------------------------------------------------------
        List<String> javadocArguments = new ArrayList<>();

        addJavadocOptions( javadocOutputDirectory, javadocArguments, sourcePaths, offlineLinks );

        // ----------------------------------------------------------------------
        // Write options file and include it in the command line
        // ----------------------------------------------------------------------

        List<String> arguments = new ArrayList<>( javadocArguments.size() + standardDocletArguments.size() );
        arguments.addAll( javadocArguments );
        arguments.addAll( standardDocletArguments );

        if ( arguments.size() > 0 )
        {
            addCommandLineOptions( cmd, arguments, javadocOutputDirectory );
        }

        // ----------------------------------------------------------------------
        // Write packages file and include it in the command line
        // ----------------------------------------------------------------------

        // MJAVADOC-365 if includes/excludes are specified, these take precedence over the default
        // package-based mode and force javadoc into file-based mode unless subpackages are
        // specified. Subpackages take precedence over file-based include/excludes. Why? Because
        // getFiles(...) returns an empty list when subpackages are specified.
        boolean includesExcludesActive =
            ( sourceFileIncludes != null && !sourceFileIncludes.isEmpty() )
                || ( sourceFileExcludes != null && !sourceFileExcludes.isEmpty() );
        if ( includesExcludesActive && !StringUtils.isEmpty( subpackages ) )
        {
            getLog().warn( "sourceFileIncludes and sourceFileExcludes have no effect when subpackages are specified!" );
            includesExcludesActive = false;
        }
        if ( !packageNames.isEmpty() && !includesExcludesActive )
        {
            addCommandLinePackages( cmd, javadocOutputDirectory, packageNames );

            // ----------------------------------------------------------------------
            // Write argfile file and include it in the command line
            // ----------------------------------------------------------------------

            List<String> specialFiles = getSpecialFiles( files );

            if ( !specialFiles.isEmpty() )
            {
                addCommandLineArgFile( cmd, javadocOutputDirectory, specialFiles );
            }
        }
        else
        {
            // ----------------------------------------------------------------------
            // Write argfile file and include it in the command line
            // ----------------------------------------------------------------------

            List<String> allFiles = new ArrayList<>();
            for ( Map.Entry<Path, Collection<String>> filesEntry : files.entrySet() )
            {
                for ( String file : filesEntry.getValue() )
                {
                    allFiles.add( filesEntry.getKey().resolve( file ).toString() );
                }
            }

            if ( !files.isEmpty() )
            {
                addCommandLineArgFile( cmd, javadocOutputDirectory, allFiles );
            }
        }

        // ----------------------------------------------------------------------
        // Execute command line
        // ----------------------------------------------------------------------

        executeJavadocCommandLine( cmd, javadocOutputDirectory );

        // delete generated javadoc files only if no error and no debug mode
        // [MJAVADOC-336] Use File.delete() instead of File.deleteOnExit() to
        // prevent these files from making their way into archives.
        if ( !debug )
        {
            for ( int i = 0; i < cmd.getArguments().length; i++ )
            {
                String arg = cmd.getArguments()[i].trim();

                if ( !arg.startsWith( "@" ) )
                {
                    continue;
                }

                File argFile = new File( javadocOutputDirectory, arg.substring( 1 ) );
                if ( argFile.exists() )
                {
                    argFile.delete();
                }
            }

            File scriptFile = new File( javadocOutputDirectory, DEBUG_JAVADOC_SCRIPT_NAME );
            if ( scriptFile.exists() )
            {
                scriptFile.delete();
            }
        }
        if ( applyJavadocSecurityFix )
        {
            // finally, patch the Javadoc vulnerability in older Javadoc tools (CVE-2013-1571):
            try
            {
                final int patched = fixFrameInjectionBug( javadocOutputDirectory, getDocencoding() );
                if ( patched > 0 )
                {
                    getLog().info(
                        String.format( "Fixed Javadoc frame injection vulnerability (CVE-2013-1571) in %d files.",
                                       patched ) );
                }
            }
            catch ( IOException e )
            {
                throw new MavenReportException( "Failed to patch javadocs vulnerability: " + e.getMessage(), e );
            }
        }
        else
        {
          getLog().info( "applying javadoc security fix has been disabled" );
        }
    }

    /**
     * Method to get the files on the specified source paths
     *
     * @param sourcePaths a Collection that contains the paths to the source files
     * @return a List that contains the specific path for every source file
     * @throws MavenReportException {@link MavenReportException} issue while generating report
     */
    protected Map<Path, Collection<String>> getFiles( Collection<Path> sourcePaths )
        throws MavenReportException
    {
        Map<Path, Collection<String>> mappedFiles = new LinkedHashMap<>( sourcePaths.size() );
        if ( StringUtils.isEmpty( subpackages ) )
        {
            Collection<String> excludedPackages = getExcludedPackages();

            final boolean autoExclude;
            if ( release != null )
            {
                autoExclude = JavaVersion.parse( release ).isBefore( "9" );
            }
            else if ( source != null )
            {
                autoExclude = JavaVersion.parse( source ).isBefore( "9" );
            }
            else
            {
                autoExclude = false;
            }

            for ( Path sourcePath : sourcePaths )
            {
                File sourceDirectory = sourcePath.toFile();
                List<String> files = new ArrayList<>( JavadocUtil.getFilesFromSource( sourceDirectory,
                        sourceFileIncludes, sourceFileExcludes,
                        excludedPackages ) );

                if ( autoExclude && files.remove( "module-info.java" ) )
                {
                    getLog().debug( "Auto exclude module-info.java due to source value" );
                }
                mappedFiles.put( sourcePath, files );
            }
        }

        return mappedFiles;
    }

    /**
     * Method to get the source paths per reactorProject. If no source path is specified in the parameter, the compile
     * source roots of the project will be used.
     *
     * @return a Map of the project absolute source paths per projects key (G:A)
     * @throws MavenReportException {@link MavenReportException} issue while generating report
     * @see JavadocUtil#pruneDirs(MavenProject, Collection)
     */
    protected Collection<JavadocModule> getSourcePaths()
        throws MavenReportException
    {
        Collection<JavadocModule> mappedSourcePaths = new ArrayList<>();

        if ( StringUtils.isEmpty( sourcepath ) )
        {
            if ( !"pom".equals( project.getPackaging()  ) )
            {
                Set<Path> sourcePaths =
                    new LinkedHashSet<>( JavadocUtil.pruneDirs( project, getProjectSourceRoots( project ) ) );

                if ( project.getExecutionProject() != null )
                {
                    sourcePaths.addAll( JavadocUtil.pruneDirs( project, getExecutionProjectSourceRoots( project ) ) );
                }

                /*
                 * Should be after the source path (i.e. -sourcepath '.../src/main/java;.../src/main/javadoc') and *not*
                 * the opposite. If not, the javadoc tool always copies doc files, even if -docfilessubdirs is not
                 * setted.
                 */
                if ( getJavadocDirectory() != null )
                {
                    File javadocDir = getJavadocDirectory();
                    if ( javadocDir.exists() && javadocDir.isDirectory() )
                    {
                        Collection<Path> l =
                            JavadocUtil.pruneDirs( project,
                                               Collections.singletonList( getJavadocDirectory().getAbsolutePath() ) );
                        sourcePaths.addAll( l );
                    }
                }
                if ( !sourcePaths.isEmpty() )
                {
                    mappedSourcePaths.add( buildJavadocModule( project, sourcePaths ) );
                }
            }

            if ( isAggregator() )
            {
                for ( MavenProject subProject : getAggregatedProjects() )
                {
                    if ( subProject != project )
                    {
                        Collection<Path> additionalSourcePaths = new ArrayList<>();

                        List<String> sourceRoots = getProjectSourceRoots( subProject );

                        if ( subProject.getExecutionProject() != null )
                        {
                            sourceRoots.addAll( getExecutionProjectSourceRoots( subProject ) );
                        }

                        ArtifactHandler artifactHandler = subProject.getArtifact().getArtifactHandler();
                        if ( "java".equals( artifactHandler.getLanguage() ) )
                        {
                            additionalSourcePaths.addAll( JavadocUtil.pruneDirs( subProject, sourceRoots ) );
                        }

                        if ( getJavadocDirectory() != null )
                        {
                            String javadocDirRelative =
                                PathUtils.toRelative( project.getBasedir(),
                                                      getJavadocDirectory().getAbsolutePath() );
                            File javadocDir = new File( subProject.getBasedir(), javadocDirRelative );
                            if ( javadocDir.exists() && javadocDir.isDirectory() )
                            {
                                Collection<Path> l = JavadocUtil.pruneDirs( subProject, Collections.singletonList(
                                        javadocDir.getAbsolutePath() ) );
                                additionalSourcePaths.addAll( l );
                            }
                        }

                        if ( !additionalSourcePaths.isEmpty() )
                        {
                            mappedSourcePaths.add( buildJavadocModule( subProject , additionalSourcePaths ) );
                        }
                    }
                }
            }

            if ( includeDependencySources )
            {
                mappedSourcePaths.addAll( getDependencySourcePaths() );
            }
        }
        else
        {
            Collection<Path> sourcePaths =
                JavadocUtil.pruneDirs( project,
                                       new ArrayList<>( Arrays.asList( JavadocUtil.splitPath( sourcepath ) ) ) );
            if ( getJavadocDirectory() != null )
            {
                Collection<Path> l = JavadocUtil.pruneDirs( project, Collections.singletonList(
                    getJavadocDirectory().getAbsolutePath() ) );
                sourcePaths.addAll( l );
            }

            if ( !sourcePaths.isEmpty() )
            {
                mappedSourcePaths.add( new JavadocModule( ArtifactUtils.versionlessKey( project.getGroupId(),
                                                                                        project.getArtifactId() ),
                                                          getClassesFile( project ),
                                                          sourcePaths ) );
            }
        }

        return mappedSourcePaths;
    }

    private JavadocModule buildJavadocModule( MavenProject project, Collection<Path> sourcePaths )
    {
        File classessFile = getClassesFile( project );
        ResolvePathResult resolvePathResult = getResolvePathResult( classessFile );
        if ( resolvePathResult == null )
        {
            return new JavadocModule( ArtifactUtils.versionlessKey( project.getGroupId(), project.getArtifactId() ),
                                      classessFile,
                                      sourcePaths );
        }
        else
        {
            return new JavadocModule( ArtifactUtils.versionlessKey( project.getGroupId(), project.getArtifactId() ),
                                      classessFile,
                                      sourcePaths,
                                      resolvePathResult.getModuleDescriptor(),
                                      resolvePathResult.getModuleNameSource() );
        }
    }

    /**
     * Recursively add the modules of the aggregatedProject to the set of aggregatedModules.
     *
     * @param aggregatedProject the project being aggregated
     * @param reactorProjectsMap map of (still) available reactor projects
     */
    private Set<MavenProject> modulesForAggregatedProject( MavenProject aggregatedProject,
                                                           Map<Path, MavenProject> reactorProjectsMap )
    {
        // Maven does not supply an easy way to get the projects representing
        // the modules of a project. So we will get the paths to the base
        // directories of the modules from the project and compare with the
        // base directories of the projects in the reactor.

        if ( aggregatedProject.getModules().isEmpty() )
        {
            return Collections.singleton( aggregatedProject );
        }

        Path basePath = aggregatedProject.getBasedir().toPath();
        List<Path> modulePaths = new LinkedList<>();
        for ( String module :  aggregatedProject.getModules() )
        {
            modulePaths.add( basePath.resolve( module ).normalize() );
        }

        Set<MavenProject> aggregatedModules = new LinkedHashSet<>();

        for ( Path modulePath : modulePaths )
        {
            MavenProject module = reactorProjectsMap.remove( modulePath );
            if ( module != null )
            {
                aggregatedModules.addAll( modulesForAggregatedProject( module, reactorProjectsMap ) );
            }
        }

        return aggregatedModules;
    }

    /**
     * Override this method to customize the configuration for resolving dependency sources. The default
     * behavior enables the resolution of -sources jar files.
     * @param config {@link SourceResolverConfig}
     * @return {@link SourceResolverConfig}
     */
    protected SourceResolverConfig configureDependencySourceResolution( final SourceResolverConfig config )
    {
        return config.withCompileSources();
    }

    /**
     * Resolve dependency sources so they can be included directly in the javadoc process. To customize this,
     * override {@link AbstractJavadocMojo#configureDependencySourceResolution(SourceResolverConfig)}.
     * @return List of source paths.
     * @throws MavenReportException {@link MavenReportException}
     */
    protected final Collection<JavadocModule> getDependencySourcePaths()
        throws MavenReportException
    {
        try
        {
            if ( sourceDependencyCacheDir.exists() )
            {
                FileUtils.forceDelete( sourceDependencyCacheDir );
                sourceDependencyCacheDir.mkdirs();
            }
        }
        catch ( IOException e )
        {
            throw new MavenReportException(
                "Failed to delete cache directory: " + sourceDependencyCacheDir + "\nReason: " + e.getMessage(), e );
        }

        final SourceResolverConfig config = getDependencySourceResolverConfig();

        try
        {
            return resourceResolver.resolveDependencySourcePaths( config );
        }
        catch ( final ArtifactResolutionException | ArtifactNotFoundException e )
        {
            throw new MavenReportException(
                "Failed to resolve one or more javadoc source/resource artifacts:\n\n" + e.getMessage(), e );
        }
    }

    /**
     * Returns a ArtifactFilter that only includes direct dependencies of this project
     * (verified via groupId and artifactId).
     *
     * @return
     */
    private TransformableFilter createDependencyArtifactFilter()
    {
        Set<Artifact> dependencyArtifacts = project.getDependencyArtifacts();

        List<String> artifactPatterns = new ArrayList<>( dependencyArtifacts.size() );
        for ( Artifact artifact : dependencyArtifacts )
        {
            artifactPatterns.add( artifact.getGroupId() + ":" + artifact.getArtifactId() );
        }

        return new PatternInclusionsFilter( artifactPatterns );
    }

    /**
     * Construct a SourceResolverConfig for resolving dependency sources and resources in a consistent
     * way, so it can be reused for both source and resource resolution.
     *
     * @since 2.7
     */
    private SourceResolverConfig getDependencySourceResolverConfig()
    {
        final List<TransformableFilter> andFilters = new ArrayList<>();

        final List<String> dependencyIncludes = dependencySourceIncludes;
        final List<String> dependencyExcludes = dependencySourceExcludes;

        if ( !includeTransitiveDependencySources || isNotEmpty( dependencyIncludes ) || isNotEmpty(
            dependencyExcludes ) )
        {
            if ( !includeTransitiveDependencySources )
            {
                andFilters.add( createDependencyArtifactFilter() );
            }

            if ( isNotEmpty( dependencyIncludes ) )
            {
                andFilters.add( new PatternInclusionsFilter( dependencyIncludes ) );
            }

            if ( isNotEmpty( dependencyExcludes ) )
            {
                andFilters.add( new PatternExclusionsFilter( dependencyExcludes ) );
            }
        }

        return configureDependencySourceResolution( new SourceResolverConfig( project,
                                                  getProjectBuildingRequest( project ),
                                                  sourceDependencyCacheDir )
                                                  .withReactorProjects( this.reactorProjects ) )
                                                  .withFilter( new AndFilter( andFilters ) );

    }

    private ProjectBuildingRequest getProjectBuildingRequest( MavenProject currentProject )
    {
        return new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() )
                          .setRemoteRepositories( currentProject.getRemoteArtifactRepositories() );
    }

    /**
     * Method that indicates whether the javadoc can be generated or not. If the project does not contain any source
     * files and no subpackages are specified, the plugin will terminate.
     *
     * @param files the project files
     * @return a boolean that indicates whether javadoc report can be generated or not
     */
    protected boolean canGenerateReport( Map<Path, Collection<String>> files )
    {
        for ( Collection<String> filesValues : files.values() )
        {
            if ( !filesValues.isEmpty() )
            {
                return true;
            }
        }

        return !StringUtils.isEmpty( subpackages );
    }

    // ----------------------------------------------------------------------
    // private methods
    // ----------------------------------------------------------------------

    /**
     * Method to get the excluded source files from the javadoc and create the argument string
     * that will be included in the javadoc commandline execution.
     *
     * @param sourcePaths the collection of paths to the source files
     * @return a String that contains the exclude argument that will be used by javadoc
     * @throws MavenReportException
     */
    private String getExcludedPackages( Collection<Path> sourcePaths )
        throws MavenReportException
    {
        List<String> excludedNames = null;

        if ( StringUtils.isNotEmpty( sourcepath ) && StringUtils.isNotEmpty( subpackages ) )
        {
            Collection<String> excludedPackages = getExcludedPackages();

            excludedNames = JavadocUtil.getExcludedPackages( sourcePaths, excludedPackages );
        }

        String excludeArg = "";
        if ( StringUtils.isNotEmpty( subpackages ) && excludedNames != null )
        {
            // add the excludedpackage names
            excludeArg = StringUtils.join( excludedNames.iterator(), ":" );
        }

        return excludeArg;
    }

    /**
     * Method to format the specified source paths that will be accepted by the javadoc tool.
     *
     * @param sourcePaths the list of paths to the source files that will be included in the javadoc.
     * @return a String that contains the formatted source path argument, separated by the System pathSeparator
     *         string (colon (<code>:</code>) on Solaris or semi-colon (<code>;</code>) on Windows).
     * @see File#pathSeparator
     */
    private String getSourcePath( Collection<Path> sourcePaths )
    {
        String sourcePath = null;

        if ( StringUtils.isEmpty( subpackages ) || StringUtils.isNotEmpty( sourcepath ) )
        {
            sourcePath = StringUtils.join( sourcePaths.iterator(), File.pathSeparator );
        }

        return sourcePath;
    }

    /**
     * Method to get the packages specified in the <code>excludePackageNames</code> parameter. The packages are split
     * with ',', ':', or ';' and then formatted.
     *
     * @return an array of String objects that contain the package names
     * @throws MavenReportException
     */
    private Collection<String> getExcludedPackages()
        throws MavenReportException
    {
        Set<String> excluded = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getExcludePackageNames() ) )
                    {
                        excluded.addAll( options.getExcludePackageNames() );
                    }
                }
            }
        }

        // for the specified excludePackageNames
        if ( StringUtils.isNotEmpty( excludePackageNames ) )
        {
            List<String> packageNames = Arrays.asList( excludePackageNames.split( "[,:;]" ) );
            excluded.addAll( trimValues( packageNames ) );
        }

        return excluded;
    }

    private static List<String> trimValues( List<String> items )
    {
        List<String> result = new ArrayList<>( items.size() );
        for ( String item : items )
        {
            String trimmed = item.trim();
            if ( StringUtils.isEmpty( trimmed ) )
            {
                continue;
            }
            result.add( trimmed );
        }
        return result;
    }

    /**
     * Method that gets the classpath and modulepath elements that will be specified in the javadoc
     * <code>-classpath</code> and <code>--module-path</code> parameter.
     * Since we have all the sources of the current reactor, it is sufficient to consider the
     * dependencies of the reactor modules, excluding the module artifacts which may not yet be available
     * when the reactor project is built for the first time.
     *
     * @return all classpath elements
     * @throws MavenReportException if any.
     */
    private Collection<File> getPathElements()
        throws MavenReportException
    {
        Set<File> classpathElements = new LinkedHashSet<>();
        Map<String, Artifact> compileArtifactMap = new LinkedHashMap<>();

        if ( isTest() )
        {
            classpathElements.addAll( getProjectBuildOutputDirs( project ) );
        }

        populateCompileArtifactMap( compileArtifactMap, project.getArtifacts() );

        if ( isAggregator() )
        {
            Collection<MavenProject> aggregatorProjects = getAggregatedProjects();

            List<String> reactorArtifacts = new ArrayList<>();
            for ( MavenProject p : aggregatorProjects )
            {
                reactorArtifacts.add( p.getGroupId() + ':' + p.getArtifactId() );
            }

            TransformableFilter dependencyFilter = new AndFilter( Arrays.asList(
                                                                     new PatternExclusionsFilter( reactorArtifacts ),
                                                                     getDependencyScopeFilter() ) );

            for ( MavenProject subProject : aggregatorProjects )
            {
                if ( subProject != project )
                {
                    File projectArtifactFile = getClassesFile( subProject );
                    if ( projectArtifactFile != null )
                    {
                        classpathElements.add( projectArtifactFile );
                    }
                    else
                    {
                        classpathElements.addAll( getProjectBuildOutputDirs( subProject ) );
                    }

                    try
                    {
                        StringBuilder sb = new StringBuilder();

                        sb.append( "Compiled artifacts for " );
                        sb.append( subProject.getGroupId() ).append( ":" );
                        sb.append( subProject.getArtifactId() ).append( ":" );
                        sb.append( subProject.getVersion() ).append( '\n' );

                        ProjectBuildingRequest buildingRequest = getProjectBuildingRequest( subProject );

                        List<Dependency> managedDependencies = null;
                        if ( subProject.getDependencyManagement() != null )
                        {
                            managedDependencies = subProject.getDependencyManagement().getDependencies();
                        }

                        for ( ArtifactResult artifactResult
                                    : dependencyResolver.resolveDependencies( buildingRequest,
                                                                              subProject.getDependencies(),
                                                                              managedDependencies,
                                                                              dependencyFilter ) )
                        {
                            populateCompileArtifactMap( compileArtifactMap,
                                                        Collections.singletonList( artifactResult.getArtifact() ) );

                            sb.append( artifactResult.getArtifact().getFile() ).append( '\n' );
                        }

                        if ( getLog().isDebugEnabled() )
                        {
                            getLog().debug( sb.toString() );
                        }

                    }
                    catch ( DependencyResolverException e )
                    {
                        throw new MavenReportException( e.getMessage(), e );
                    }
                }
            }
        }

        for ( Artifact a : compileArtifactMap.values() )
        {
            classpathElements.add( a.getFile() );
        }

        if ( additionalDependencies != null )
        {
            for ( Dependency dependency : additionalDependencies )
            {
                Artifact artifact = resolveDependency( dependency );
                getLog().debug( "add additional artifact with path " + artifact.getFile() );
                classpathElements.add( artifact.getFile() );
            }
        }

        return classpathElements;
    }

    protected ScopeFilter getDependencyScopeFilter()
    {
        return ScopeFilter.including( Artifact.SCOPE_COMPILE, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_SYSTEM );
    }

    /**
     * @param dependency {@link Dependency}
     * @return {@link Artifact}
     * @throws MavenReportException when artifact could not be resolved
     */
    public Artifact resolveDependency( Dependency dependency )
        throws MavenReportException
    {
        DefaultArtifactCoordinate coordinate = new DefaultArtifactCoordinate();
        coordinate.setGroupId( dependency.getGroupId() );
        coordinate.setArtifactId( dependency.getArtifactId() );
        coordinate.setVersion( dependency.getVersion() );
        coordinate.setClassifier( dependency.getClassifier() );
        coordinate.setExtension( artifactHandlerManager.getArtifactHandler( dependency.getType() ).getExtension() );

        try
        {
            return artifactResolver.resolveArtifact( getProjectBuildingRequest( project ), coordinate ).getArtifact();
        }
        catch ( ArtifactResolverException e )
        {
            throw new MavenReportException( "artifact resolver problem - " + e.getMessage(), e );
        }
    }


    //TODO remove the part with ToolchainManager lookup once we depend on
    //3.0.9 (have it as prerequisite). Define as regular component field then.
    protected final Toolchain getToolchain()
    {
        Toolchain tc = null;

        if ( jdkToolchain != null )
        {
            // Maven 3.3.1 has plugin execution scoped Toolchain Support
            try
            {
                Method getToolchainsMethod =
                    toolchainManager.getClass().getMethod( "getToolchains", MavenSession.class, String.class,
                                                           Map.class );

                @SuppressWarnings( "unchecked" )
                List<Toolchain> tcs =
                    (List<Toolchain>) getToolchainsMethod.invoke( toolchainManager, session, "jdk",
                                                                  jdkToolchain );

                if ( tcs != null && tcs.size() > 0 )
                {
                    tc = tcs.get( 0 );
                }
            }
            catch ( SecurityException | ReflectiveOperationException e )
            {
                // ignore
            }
        }

        if ( tc == null )
        {
            tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
        }

        return tc;
    }

    /**
     * Method to put the artifacts in the hashmap.
     *
     * @param compileArtifactMap the hashmap that will contain the artifacts
     * @param artifactList       the list of artifacts that will be put in the map
     * @throws MavenReportException if any
     */
    private void populateCompileArtifactMap( Map<String, Artifact> compileArtifactMap,
                                             Collection<Artifact> artifactList )
        throws MavenReportException
    {
        if ( artifactList == null )
        {
            return;
        }

        for ( Artifact newArtifact : artifactList )
        {
            File file = newArtifact.getFile();

            if ( file == null )
            {
                throw new MavenReportException(
                    "Error in plugin descriptor - " + "dependency was not resolved for artifact: "
                        + newArtifact.getGroupId() + ":" + newArtifact.getArtifactId() + ":"
                        + newArtifact.getVersion() );
            }

            if ( compileArtifactMap.get( newArtifact.getDependencyConflictId() ) != null )
            {
                Artifact oldArtifact = compileArtifactMap.get( newArtifact.getDependencyConflictId() );

                ArtifactVersion oldVersion = new DefaultArtifactVersion( oldArtifact.getVersion() );
                ArtifactVersion newVersion = new DefaultArtifactVersion( newArtifact.getVersion() );
                if ( newVersion.compareTo( oldVersion ) > 0 )
                {
                    compileArtifactMap.put( newArtifact.getDependencyConflictId(), newArtifact );
                }
            }
            else
            {
                compileArtifactMap.put( newArtifact.getDependencyConflictId(), newArtifact );
            }
        }
    }

    /**
     * Method that sets the bottom text that will be displayed on the bottom of the
     * javadocs.
     *
     * @return a String that contains the text that will be displayed at the bottom of the javadoc
     */
    private String getBottomText()
    {
        final String inceptionYear = project.getInceptionYear();

        // get Reproducible Builds outputTimestamp date value or the current local date.
        final LocalDate localDate = MavenArchiver.parseBuildOutputTimestamp( outputTimestamp )
            .map( instant -> instant.atZone( ZoneOffset.UTC ).toLocalDate() )
            .orElseGet( LocalDate::now );

        final String currentYear = Integer.toString( localDate.getYear() );

        String theBottom = StringUtils.replace( this.bottom, "{currentYear}", currentYear );

        if ( ( inceptionYear == null ) || inceptionYear.equals( currentYear ) )
        {
            theBottom = StringUtils.replace( theBottom, "{inceptionYear}&#x2013;", "" );
        }
        else
        {
            theBottom = StringUtils.replace( theBottom, "{inceptionYear}", inceptionYear );
        }

        if ( project.getOrganization() == null )
        {
            theBottom = StringUtils.replace( theBottom, " {organizationName}", "" );
        }
        else
        {
            if ( StringUtils.isNotEmpty( project.getOrganization().getName() ) )
            {
                if ( StringUtils.isNotEmpty( project.getOrganization().getUrl() ) )
                {
                    theBottom = StringUtils.replace( theBottom, "{organizationName}",
                                                     "<a href=\"" + project.getOrganization().getUrl() + "\">"
                                                         + project.getOrganization().getName() + "</a>" );
                }
                else
                {
                    theBottom =
                        StringUtils.replace( theBottom, "{organizationName}", project.getOrganization().getName() );
                }
            }
            else
            {
                theBottom = StringUtils.replace( theBottom, " {organizationName}", "" );
            }
        }

        return theBottom;
    }

    /**
     * Method to get the stylesheet path file to be used by the Javadoc Tool.
     * <br/>
     * If the {@code stylesheetfile} is empty, return the file as String definded by {@code stylesheet} value.
     * <br/>
     * If the {@code stylesheetfile} is defined, return the file as String.
     * <br/>
     * Note: since 2.6, the {@code stylesheetfile} could be a path from a resource in the project source
     * directories (i.e. <code>src/main/java</code>, <code>src/main/resources</code> or <code>src/main/javadoc</code>)
     * or from a resource in the Javadoc plugin dependencies.
     *
     * @param javadocOutputDirectory the output directory
     * @return the stylesheet file absolute path as String.
     * @see #getResource(List, String)
     */
    private Optional<File> getStylesheetFile( final File javadocOutputDirectory )
    {
        if ( StringUtils.isEmpty( stylesheetfile ) )
        {
            if ( "java".equalsIgnoreCase( stylesheet ) )
            {
                // use the default Javadoc tool stylesheet
                return Optional.empty();
            }

            // maven, see #copyDefaultStylesheet(File)
            return Optional.of( new File( javadocOutputDirectory, DEFAULT_CSS_NAME ) );
        }

        if ( new File( stylesheetfile ).exists() )
        {
            return Optional.of( new File( stylesheetfile ) );
        }

        return getResource( new File( javadocOutputDirectory, DEFAULT_CSS_NAME ), stylesheetfile );
    }

    private void addAddStyleSheets( List<String> arguments ) throws MavenReportException
    {
        if ( addStylesheets == null )
        {
            return;
        }

        for ( String addStylesheet : addStylesheets )
        {
            Optional<File> styleSheet = getAddStylesheet( getJavadocDirectory(), addStylesheet );

            if ( styleSheet.isPresent() )
            {
                addArgIfNotEmpty( arguments, "--add-stylesheet",
                                  JavadocUtil.quotedPathArgument( styleSheet.get().getAbsolutePath() ),
                                  JavaVersion.parse( "10" ) );
            }
        }
    }


    private Optional<File> getAddStylesheet( final File javadocOutputDirectory, final String stylesheet )
            throws MavenReportException
    {
        if ( StringUtils.isEmpty( stylesheet ) )
        {
            return Optional.empty();
        }

        File addstylesheetfile = new File( getJavadocDirectory(), stylesheet );
        if ( addstylesheetfile.exists() )
        {
            Optional<File> stylesheetfile = getStylesheetFile( javadocOutputDirectory );
            if ( stylesheetfile.isPresent() )
            {
                if ( stylesheetfile.get().getName().equals( addstylesheetfile.getName() ) )
                {
                    throw new MavenReportException( "additional stylesheet must have a different name "
                                                        + "than stylesheetfile: " + stylesheetfile.get().getName() );
                }
            }

            return Optional.of( addstylesheetfile );
        }

        throw new MavenReportException( "additional stylesheet file does not exist: "
                                            + addstylesheetfile.getAbsolutePath() );
    }

    /**
     * Method to get the help file to be used by the Javadoc Tool.
     * <br/>
     * Since 2.6, the {@code helpfile} could be a path from a resource in the project source
     * directories (i.e. <code>src/main/java</code>, <code>src/main/resources</code> or <code>src/main/javadoc</code>)
     * or from a resource in the Javadoc plugin dependencies.
     *
     * @param javadocOutputDirectory the output directory.
     * @return the help file absolute path as String.
     * @see #getResource(File, String)
     * @since 2.6
     */
    private Optional<File> getHelpFile( final File javadocOutputDirectory )
    {
        if ( StringUtils.isEmpty( helpfile ) )
        {
            return Optional.empty();
        }

        if ( new File( helpfile ).exists() )
        {
            return Optional.of( new File( helpfile ) );
        }

        return getResource( new File( javadocOutputDirectory, "help-doc.html" ), helpfile );
    }

    /**
     * Method to get the access level for the classes and members to be shown in the generated javadoc.
     * If the specified access level is not public, protected, package or private, the access level
     * is set to protected.
     *
     * @return the access level
     */
    private String getAccessLevel()
    {
        String accessLevel;
        if ( "public".equalsIgnoreCase( show ) || "protected".equalsIgnoreCase( show ) || "package".equalsIgnoreCase(
            show ) || "private".equalsIgnoreCase( show ) )
        {
            accessLevel = "-" + show;
        }
        else
        {
            if ( getLog().isErrorEnabled() )
            {
                getLog().error( "Unrecognized access level to show '" + show + "'. Defaulting to protected." );
            }
            accessLevel = "-protected";
        }

        return accessLevel;
    }

    /**
     * Method to get the path of the bootclass artifacts used in the <code>-bootclasspath</code> option.
     *
     * @return a string that contains bootclass path, separated by the System pathSeparator string
     *         (colon (<code>:</code>) on Solaris or semi-colon (<code>;</code>) on Windows).
     * @throws MavenReportException if any
     * @see File#pathSeparator
     */
    private String getBootclassPath()
        throws MavenReportException
    {
        Set<BootclasspathArtifact> bootclasspathArtifacts = collectBootClasspathArtifacts();

        List<String> bootclassPath = new ArrayList<>();
        for ( BootclasspathArtifact aBootclasspathArtifact : bootclasspathArtifacts )
        {
            if ( ( StringUtils.isNotEmpty( aBootclasspathArtifact.getGroupId() ) ) && ( StringUtils.isNotEmpty(
                aBootclasspathArtifact.getArtifactId() ) ) && ( StringUtils.isNotEmpty(
                aBootclasspathArtifact.getVersion() ) ) )
            {
                bootclassPath.addAll( getArtifactsAbsolutePath( aBootclasspathArtifact ) );
            }
        }

        bootclassPath = JavadocUtil.pruneFiles( bootclassPath );

        StringBuilder path = new StringBuilder();
        path.append( StringUtils.join( bootclassPath.iterator(), File.pathSeparator ) );

        if ( StringUtils.isNotEmpty( bootclasspath ) )
        {
            path.append( JavadocUtil.unifyPathSeparator( bootclasspath ) );
        }

        return path.toString();
    }

    /**
     * Method to get the path of the doclet artifacts used in the <code>-docletpath</code> option.
     * <p/>
     * Either docletArtifact or doclectArtifacts can be defined and used, not both, docletArtifact
     * takes precedence over doclectArtifacts. docletPath is always appended to any result path
     * definition.
     *
     * @return a string that contains doclet path, separated by the System pathSeparator string
     *         (colon (<code>:</code>) on Solaris or semi-colon (<code>;</code>) on Windows).
     * @throws MavenReportException if any
     * @see File#pathSeparator
     */
    private String getDocletPath()
        throws MavenReportException
    {
        Set<DocletArtifact> docletArtifacts = collectDocletArtifacts();
        List<String> pathParts = new ArrayList<>();

        for ( DocletArtifact docletArtifact : docletArtifacts )
        {
            if ( !isDocletArtifactEmpty( docletArtifact ) )
            {
                pathParts.addAll( getArtifactsAbsolutePath( docletArtifact ) );
            }
        }

        if ( !StringUtils.isEmpty( docletPath ) )
        {
            pathParts.add( JavadocUtil.unifyPathSeparator( docletPath ) );
        }

        String path = StringUtils.join( pathParts.iterator(), File.pathSeparator );

        if ( StringUtils.isEmpty( path ) && getLog().isWarnEnabled() )
        {
            getLog().warn( "No docletpath option was found. Please review <docletpath/> or <docletArtifact/>"
                               + " or <doclets/>." );
        }

        return path;
    }

    /**
     * Verify if a doclet artifact is empty or not
     *
     * @param aDocletArtifact could be null
     * @return <code>true</code> if aDocletArtifact or the groupId/artifactId/version of the doclet artifact is null,
     *         <code>false</code> otherwise.
     */
    private boolean isDocletArtifactEmpty( DocletArtifact aDocletArtifact )
    {
        if ( aDocletArtifact == null )
        {
            return true;
        }

        return StringUtils.isEmpty( aDocletArtifact.getGroupId() ) && StringUtils.isEmpty(
            aDocletArtifact.getArtifactId() ) && StringUtils.isEmpty( aDocletArtifact.getVersion() );
    }

    /**
     * Method to get the path of the taglet artifacts used in the <code>-tagletpath</code> option.
     *
     * @return a string that contains taglet path, separated by the System pathSeparator string
     *         (colon (<code>:</code>) on Solaris or semi-colon (<code>;</code>) on Windows).
     * @throws MavenReportException if any
     * @see File#pathSeparator
     */
    private String getTagletPath()
        throws MavenReportException
    {
        Set<TagletArtifact> tArtifacts = collectTagletArtifacts();
        Collection<String> pathParts = new ArrayList<>();

        for ( TagletArtifact tagletArtifact : tArtifacts )
        {
            if ( ( tagletArtifact != null ) && ( StringUtils.isNotEmpty( tagletArtifact.getGroupId() ) )
                && ( StringUtils.isNotEmpty( tagletArtifact.getArtifactId() ) ) && ( StringUtils.isNotEmpty(
                tagletArtifact.getVersion() ) ) )
            {
                pathParts.addAll( getArtifactsAbsolutePath( tagletArtifact ) );
            }
        }

        Set<Taglet> taglets = collectTaglets();
        for ( Taglet taglet : taglets )
        {
            if ( taglet == null )
            {
                continue;
            }

            if ( ( taglet.getTagletArtifact() != null ) && ( StringUtils.isNotEmpty(
                taglet.getTagletArtifact().getGroupId() ) ) && ( StringUtils.isNotEmpty(
                taglet.getTagletArtifact().getArtifactId() ) ) && ( StringUtils.isNotEmpty(
                taglet.getTagletArtifact().getVersion() ) ) )
            {
                pathParts.addAll( JavadocUtil.pruneFiles( getArtifactsAbsolutePath( taglet.getTagletArtifact() ) ) );
            }
            else if ( StringUtils.isNotEmpty( taglet.getTagletpath() ) )
            {
                for ( Path dir : JavadocUtil.pruneDirs( project, Collections.singletonList( taglet.getTagletpath() ) ) )
                {
                    pathParts.add( dir.toString()  );
                }
            }
        }

        StringBuilder path = new StringBuilder();
        path.append( StringUtils.join( pathParts.iterator(), File.pathSeparator ) );

        if ( StringUtils.isNotEmpty( tagletpath ) )
        {
            path.append( JavadocUtil.unifyPathSeparator( tagletpath ) );
        }

        return path.toString();
    }

    private Set<String> collectLinks()
        throws MavenReportException
    {
        Set<String> links = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getLinks() ) )
                    {
                        links.addAll( options.getLinks() );
                    }
                }
            }
        }

        if ( isNotEmpty( this.links ) )
        {
            links.addAll( this.links );
        }

        links.addAll( getDependenciesLinks() );

        return followLinks( links );
    }

    private Set<Group> collectGroups()
        throws MavenReportException
    {
        Set<Group> groups = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getGroups() ) )
                    {
                        groups.addAll( options.getGroups() );
                    }
                }
            }
        }

        if ( this.groups != null && this.groups.length > 0 )
        {
            groups.addAll( Arrays.asList( this.groups ) );
        }

        return groups;
    }

    private Set<ResourcesArtifact> collectResourcesArtifacts()
        throws MavenReportException
    {
        Set<ResourcesArtifact> result = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getResourcesArtifacts() ) )
                    {
                        result.addAll( options.getResourcesArtifacts() );
                    }
                }
            }
        }

        if ( this.resourcesArtifacts != null && this.resourcesArtifacts.length > 0 )
        {
            result.addAll( Arrays.asList( this.resourcesArtifacts ) );
        }

        return result;
    }

    private Set<BootclasspathArtifact> collectBootClasspathArtifacts()
        throws MavenReportException
    {
        Set<BootclasspathArtifact> result = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getBootclasspathArtifacts() ) )
                    {
                        result.addAll( options.getBootclasspathArtifacts() );
                    }
                }
            }
        }

        if ( this.bootclasspathArtifacts != null && this.bootclasspathArtifacts.length > 0 )
        {
            result.addAll( Arrays.asList( this.bootclasspathArtifacts ) );
        }

        return result;
    }

    private Set<OfflineLink> collectOfflineLinks()
        throws MavenReportException
    {
        Set<OfflineLink> result = new LinkedHashSet<>();

        OfflineLink javaApiLink = getDefaultJavadocApiLink();
        if ( javaApiLink != null )
        {
            result.add( javaApiLink );
        }

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getOfflineLinks() ) )
                    {
                        result.addAll( options.getOfflineLinks() );
                    }
                }
            }
        }

        if ( this.offlineLinks != null && this.offlineLinks.length > 0 )
        {
            result.addAll( Arrays.asList( this.offlineLinks ) );
        }

        return result;
    }

    private Set<Tag> collectTags()
        throws MavenReportException
    {
        Set<Tag> tags = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getTags() ) )
                    {
                        tags.addAll( options.getTags() );
                    }
                }
            }
        }

        if ( this.tags != null && this.tags.length > 0 )
        {
            tags.addAll( Arrays.asList( this.tags ) );
        }

        return tags;
    }

    private Set<TagletArtifact> collectTagletArtifacts()
        throws MavenReportException
    {
        Set<TagletArtifact> tArtifacts = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getTagletArtifacts() ) )
                    {
                        tArtifacts.addAll( options.getTagletArtifacts() );
                    }
                }
            }
        }

        if ( tagletArtifact != null )
        {
            tArtifacts.add( tagletArtifact );
        }

        if ( tagletArtifacts != null && tagletArtifacts.length > 0 )
        {
            tArtifacts.addAll( Arrays.asList( tagletArtifacts ) );
        }

        return tArtifacts;
    }

    private Set<DocletArtifact> collectDocletArtifacts()
        throws MavenReportException
    {
        Set<DocletArtifact> dArtifacts = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getDocletArtifacts() ) )
                    {
                        dArtifacts.addAll( options.getDocletArtifacts() );
                    }
                }
            }
        }

        if ( docletArtifact != null )
        {
            dArtifacts.add( docletArtifact );
        }

        if ( docletArtifacts != null && docletArtifacts.length > 0 )
        {
            dArtifacts.addAll( Arrays.asList( docletArtifacts ) );
        }

        return dArtifacts;
    }

    private Set<Taglet> collectTaglets()
        throws MavenReportException
    {
        Set<Taglet> result = new LinkedHashSet<>();

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getTaglets() ) )
                    {
                        result.addAll( options.getTaglets() );
                    }
                }
            }
        }

        if ( taglets != null && taglets.length > 0 )
        {
            result.addAll( Arrays.asList( taglets ) );
        }

        return result;
    }

    /**
     * Return the Javadoc artifact path and its transitive dependencies path from the local repository
     *
     * @param javadocArtifact not null
     * @return a list of locale artifacts absolute path
     * @throws MavenReportException if any
     */
    private List<String> getArtifactsAbsolutePath( JavadocPathArtifact javadocArtifact )
        throws MavenReportException
    {
        if ( ( StringUtils.isEmpty( javadocArtifact.getGroupId() ) ) && ( StringUtils.isEmpty(
            javadocArtifact.getArtifactId() ) ) && ( StringUtils.isEmpty( javadocArtifact.getVersion() ) ) )
        {
            return Collections.emptyList();
        }

        List<String> path = new ArrayList<>();

        try
        {
            Artifact artifact = createAndResolveArtifact( javadocArtifact );
            path.add( artifact.getFile().getAbsolutePath() );

            DefaultDependableCoordinate coordinate = new DefaultDependableCoordinate();
            coordinate.setGroupId( javadocArtifact.getGroupId() );
            coordinate.setArtifactId( javadocArtifact.getArtifactId() );
            coordinate.setVersion( javadocArtifact.getVersion() );

            Iterable<ArtifactResult> deps =
                dependencyResolver.resolveDependencies( getProjectBuildingRequest( project ), coordinate,
                                                        ScopeFilter.including( "compile", "provided" ) );
            for ( ArtifactResult a : deps )
            {
                path.add( a.getArtifact().getFile().getAbsolutePath() );
            }

            return path;
        }
        catch ( ArtifactResolverException e )
        {
            throw new MavenReportException( "Unable to resolve artifact:" + javadocArtifact, e );
        }
        catch ( DependencyResolverException e )
        {
            throw new MavenReportException( "Unable to resolve dependencies for:" + javadocArtifact, e );
        }
    }

    /**
     * creates an {@link Artifact} representing the configured {@link JavadocPathArtifact} and resolves it.
     *
     * @param javadocArtifact the {@link JavadocPathArtifact} to resolve
     * @return a resolved {@link Artifact}
     * @throws ArtifactResolverException issue while resolving artifact
     */
    private Artifact createAndResolveArtifact( JavadocPathArtifact javadocArtifact )
        throws ArtifactResolverException
    {
        DefaultArtifactCoordinate coordinate = new DefaultArtifactCoordinate();
        coordinate.setGroupId( javadocArtifact.getGroupId() );
        coordinate.setArtifactId( javadocArtifact.getArtifactId() );
        coordinate.setVersion( javadocArtifact.getVersion() );
        coordinate.setClassifier( javadocArtifact.getClassifier() );

        return artifactResolver.resolveArtifact( getProjectBuildingRequest( project ), coordinate ).getArtifact();
    }

    /**
     * Method that adds/sets the java memory parameters in the command line execution.
     *
     * @param cmd    the command line execution object where the argument will be added
     * @param arg    the argument parameter name
     * @param memory the JVM memory value to be set
     * @see JavadocUtil#parseJavadocMemory(String)
     */
    private void addMemoryArg( Commandline cmd, String arg, String memory )
    {
        if ( StringUtils.isNotEmpty( memory ) )
        {
            try
            {
                cmd.createArg().setValue( "-J" + arg + JavadocUtil.parseJavadocMemory( memory ) );
            }
            catch ( IllegalArgumentException e )
            {
                if ( getLog().isErrorEnabled() )
                {
                    getLog().error( "Malformed memory pattern for '" + arg + memory + "'. Ignore this option." );
                }
            }
        }
    }

    /**
     * Method that adds/sets the javadoc proxy parameters in the command line execution.
     *
     * @param cmd the command line execution object where the argument will be added
     */
    private void addProxyArg( Commandline cmd )
    {
        if ( settings == null || settings.getProxies().isEmpty() )
        {
            return;
        }

        Map<String, Proxy> activeProxies = new HashMap<>();

        for ( Proxy proxy : settings.getProxies() )
        {
            if ( proxy.isActive() )
            {
                String protocol = proxy.getProtocol();

                if ( !activeProxies.containsKey( protocol ) )
            {
                    activeProxies.put( protocol, proxy );
                }
            }
            }

        if ( activeProxies.containsKey( "https" ) )
        {
            Proxy httpsProxy = activeProxies.get( "https" );
            if ( StringUtils.isNotEmpty( httpsProxy.getHost() ) )
            {
                cmd.createArg().setValue( "-J-Dhttps.proxyHost=" + httpsProxy.getHost() );
                cmd.createArg().setValue( "-J-Dhttps.proxyPort=" + httpsProxy.getPort() );

                if ( StringUtils.isNotEmpty( httpsProxy.getNonProxyHosts() )
                     && ( !activeProxies.containsKey( "http" )
                          || StringUtils.isEmpty( activeProxies.get( "http" ).getNonProxyHosts() ) ) )
                {
                    cmd.createArg().setValue( "-J-Dhttp.nonProxyHosts=\""
                                              + httpsProxy.getNonProxyHosts().replace( "|", "^|" ) + "\"" );
                }
            }
        }

        if ( activeProxies.containsKey( "http" ) )
        {
            Proxy httpProxy = activeProxies.get( "http" );
            if ( StringUtils.isNotEmpty( httpProxy.getHost() ) )
            {
                cmd.createArg().setValue( "-J-Dhttp.proxyHost=" + httpProxy.getHost() );
                cmd.createArg().setValue( "-J-Dhttp.proxyPort=" + httpProxy.getPort() );

                if ( !activeProxies.containsKey( "https" ) )
            {
                    cmd.createArg().setValue( "-J-Dhttps.proxyHost=" + httpProxy.getHost() );
                    cmd.createArg().setValue( "-J-Dhttps.proxyPort=" + httpProxy.getPort() );
                }

                if ( StringUtils.isNotEmpty( httpProxy.getNonProxyHosts() ) )
                {
                    cmd.createArg().setValue( "-J-Dhttp.nonProxyHosts=\""
                                              + httpProxy.getNonProxyHosts().replace( "|", "^|" ) + "\"" );
                }
            }
        }

        // We bravely ignore FTP because no one (probably) uses FTP for Javadoc
    }

    /**
     * Get the path of the Javadoc tool executable depending the user entry or try to find it depending the OS
     * or the <code>java.home</code> system property or the <code>JAVA_HOME</code> environment variable.
     *
     * @return the path of the Javadoc tool
     * @throws IOException if not found
     */
    private String getJavadocExecutable()
        throws IOException
    {
        Toolchain tc = getToolchain();

        if ( tc != null )
        {
            getLog().info( "Toolchain in maven-javadoc-plugin: " + tc );
            if ( javadocExecutable != null )
            {
                getLog().warn( "Toolchains are ignored, 'javadocExecutable' parameter is set to " + javadocExecutable );
            }
            else
            {
                javadocExecutable = tc.findTool( "javadoc" );
            }
        }

        String javadocCommand = "javadoc" + ( SystemUtils.IS_OS_WINDOWS ? ".exe" : "" );

        File javadocExe;

        // ----------------------------------------------------------------------
        // The javadoc executable is defined by the user
        // ----------------------------------------------------------------------
        if ( StringUtils.isNotEmpty( javadocExecutable ) )
        {
            javadocExe = new File( javadocExecutable );

            if ( javadocExe.isDirectory() )
            {
                javadocExe = new File( javadocExe, javadocCommand );
            }

            if ( SystemUtils.IS_OS_WINDOWS && javadocExe.getName().indexOf( '.' ) < 0 )
            {
                javadocExe = new File( javadocExe.getPath() + ".exe" );
            }

            if ( !javadocExe.isFile() )
            {
                throw new IOException( "The javadoc executable '" + javadocExe
                    + "' doesn't exist or is not a file. Verify the <javadocExecutable/> parameter." );
            }

            return javadocExe.getAbsolutePath();
        }
        // CHECKSTYLE_OFF: LineLength
        // ----------------------------------------------------------------------
        // Try to find javadocExe from System.getProperty( "java.home" )
        // "java.home" is the "Installation directory for Java Runtime
        // Environment (JRE)" used to run this code. I.e. it is the parent of
        // the directory containing the `java` command used to start this
        // application. It does not necessarily have any relation to the
        // environment variable JAVA_HOME.
        //
        // In Java 8 and below the JRE is separate from the JDK. When
        // installing the JDK to my-dir/ the javadoc command is installed in
        // my-dir/bin/javadoc, the JRE is installed to my-dir/jre, and hence
        // the java command is installed to my-dir/jre/bin/java. In this
        // configuration "java.home" is mydir/jre/, threfore the relative path
        // to the javadoc command is ../bin/javadoc.
        //
        // In Java 9 and above the JRE is no longer in a subdirectory of the
        // JDK, i.e. the JDK and the JDK are merged. In this case the java
        // command is installed to my-dir/bin/java along side the javadoc
        // command. So the relative path from "java.home" to the javadoc
        // command is bin/javadoc.
        //
        // References
        //
        // "System Properties" in "The Java Tutorials"
        // https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html
        //
        // "Changes to the Installed JDK/JRE Image" in "JDK 9 Migration Guide"
        // https://docs.oracle.com/javase/9/migrate/toc.htm?xd_co_f=122a7174-9132-4acd-b122-fac02f8c4fef#JSMIG-GUID-D867DCCC-CEB5-4AFA-9D11-9C62B7A3FAB1
        //
        // "JEP 220: Modular Run-Time Images"
        // http://openjdk.java.net/jeps/220
        // ----------------------------------------------------------------------
        // For IBM's JDK 1.2
        // CHECKSTYLE_ON: LineLength
        if ( SystemUtils.IS_OS_AIX )
        {
            javadocExe =
                new File( SystemUtils.getJavaHome() + File.separator + ".." + File.separator + "sh", javadocCommand );
        }
        // For Apple's JDK 1.6.x (and older?) on Mac OSX
        // CHECKSTYLE_OFF: MagicNumber
        else if ( SystemUtils.IS_OS_MAC_OSX && !JavaVersion.JAVA_SPECIFICATION_VERSION.isAtLeast( "1.7" ) )
        // CHECKSTYLE_ON: MagicNumber
        {
            javadocExe = new File( SystemUtils.getJavaHome() + File.separator + "bin", javadocCommand );
        }
        else if ( isJavaVersionAtLeast( org.apache.commons.lang3.JavaVersion.JAVA_9 ) )
        {
            javadocExe =
                new File( SystemUtils.getJavaHome() + File.separator + "bin", javadocCommand );
        }
        else
        {
            // Java <= 8
            javadocExe =
                new File( SystemUtils.getJavaHome() + File.separator + ".." + File.separator + "bin", javadocCommand );
        }

        // ----------------------------------------------------------------------
        // Try to find javadocExe from JAVA_HOME environment variable
        // ----------------------------------------------------------------------
        if ( !javadocExe.exists() || !javadocExe.isFile() )
        {
            Properties env = CommandLineUtils.getSystemEnvVars();
            String javaHome = env.getProperty( "JAVA_HOME" );
            if ( StringUtils.isEmpty( javaHome ) )
            {
                throw new IOException( "The environment variable JAVA_HOME is not correctly set." );
            }
            if ( ( !new File( javaHome ).getCanonicalFile().exists() )
                || ( new File( javaHome ).getCanonicalFile().isFile() ) )
            {
                throw new IOException( "The environment variable JAVA_HOME=" + javaHome
                    + " doesn't exist or is not a valid directory." );
            }

            javadocExe = new File( javaHome + File.separator + "bin", javadocCommand );
        }

        if ( !javadocExe.getCanonicalFile().exists() || !javadocExe.getCanonicalFile().isFile() )
        {
            throw new IOException( "The javadoc executable '" + javadocExe
                + "' doesn't exist or is not a file. Verify the JAVA_HOME environment variable." );
        }

        return javadocExe.getAbsolutePath();
    }

    /**
     * Set a new value for <code>javadocRuntimeVersion</code>
     *
     * @param jExecutable not null
     * @throws MavenReportException if not found
     * @see JavadocUtil#getJavadocVersion(File)
     */
    private void setFJavadocVersion( File jExecutable )
        throws MavenReportException
    {
        JavaVersion jVersion;
        try
        {
            jVersion = JavadocUtil.getJavadocVersion( jExecutable );
        }
        catch ( IOException | CommandLineException | IllegalArgumentException e )
        {
            if ( getLog().isWarnEnabled() )
            {
                getLog().warn( "Unable to find the javadoc version: " + e.getMessage() );
                getLog().warn( "Using the Java version instead of, i.e. " + JAVA_VERSION );
            }
            jVersion = JAVA_VERSION;
        }

        if ( StringUtils.isNotEmpty( javadocVersion ) )
        {
            try
            {
                javadocRuntimeVersion = JavaVersion.parse( javadocVersion );
            }
            catch ( NumberFormatException e )
            {
                throw new MavenReportException( "Unable to parse javadoc version: " + e.getMessage(), e );
            }

            if ( javadocRuntimeVersion.compareTo( jVersion ) != 0 && getLog().isWarnEnabled() )
            {
                getLog().warn( "Are you sure about the <javadocVersion/> parameter? It seems to be " + jVersion );
            }
        }
        else
        {
            javadocRuntimeVersion = jVersion;
        }
    }

    /**
     * Is the Javadoc version at least the requested version.
     *
     * @param requiredVersion the required version, for example 1.5f
     * @return <code>true</code> if the javadoc version is equal or greater than the
     *         required version
     */
    private boolean isJavaDocVersionAtLeast( JavaVersion requiredVersion )
    {
        return JAVA_VERSION.compareTo( requiredVersion ) >= 0;
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * conditionally based on the given flag.
     *
     * @param arguments a list of arguments, not null
     * @param b         the flag which controls if the argument is added or not.
     * @param value     the argument value to be added.
     */
    private void addArgIf( List<String> arguments, boolean b, String value )
    {
        if ( b )
        {
            arguments.add( value );
        }
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * regarding the requested Java version.
     *
     * @param arguments           a list of arguments, not null
     * @param b                   the flag which controls if the argument is added or not.
     * @param value               the argument value to be added.
     * @param requiredJavaVersion the required Java version, for example 1.31f or 1.4f
     * @see #addArgIf(List, boolean, String)
     * @see #isJavaDocVersionAtLeast(JavaVersion)
     */
    private void addArgIf( List<String> arguments, boolean b, String value, JavaVersion requiredJavaVersion )
    {
        if ( b )
        {
            if ( isJavaDocVersionAtLeast( requiredJavaVersion ) )
            {
                addArgIf( arguments, true, value );
            }
            else
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( value + " option is not supported on Java version < " + requiredJavaVersion
                                       + ". Ignore this option." );
                }
            }
        }
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * if the the value is not null or empty.
     * <p/>
     * Moreover, the value could be comma separated.
     *
     * @param arguments a list of arguments, not null
     * @param key       the argument name.
     * @param value     the argument value to be added.
     * @see #addArgIfNotEmpty(List, String, String, boolean)
     */
    private void addArgIfNotEmpty( List<String> arguments, String key, String value )
    {
        addArgIfNotEmpty( arguments, key, value, false );
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * if the the value is not null or empty.
     * <p/>
     * Moreover, the value could be comma separated.
     *
     * @param arguments           a list of arguments, not null
     * @param key                 the argument name.
     * @param value               the argument value to be added.
     * @param repeatKey           repeat or not the key in the command line
     * @param splitValue          if <code>true</code> given value will be tokenized by comma
     * @param requiredJavaVersion the required Java version, for example 1.31f or 1.4f
     * @see #addArgIfNotEmpty(List, String, String, boolean, boolean)
     * @see #isJavaDocVersionAtLeast(JavaVersion)
     */
    private void addArgIfNotEmpty( List<String> arguments, String key, String value, boolean repeatKey,
                                   boolean splitValue, JavaVersion requiredJavaVersion )
    {
        if ( StringUtils.isNotEmpty( value ) )
        {
            if ( isJavaDocVersionAtLeast( requiredJavaVersion ) )
            {
                addArgIfNotEmpty( arguments, key, value, repeatKey, splitValue );
            }
            else
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( key + " option is not supported on Java version < " + requiredJavaVersion
                                       + ". Ignore this option." );
                }
            }
        }
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * if the the value is not null or empty.
     * <p/>
     * Moreover, the value could be comma separated.
     *
     * @param arguments  a list of arguments, not null
     * @param key        the argument name.
     * @param value      the argument value to be added.
     * @param repeatKey  repeat or not the key in the command line
     * @param splitValue if <code>true</code> given value will be tokenized by comma
     */
    private void addArgIfNotEmpty( List<String> arguments, String key, String value, boolean repeatKey,
                                   boolean splitValue )
    {
        if ( StringUtils.isNotEmpty( value ) )
        {
            if ( StringUtils.isNotEmpty( key ) )
            {
                arguments.add( key );
            }

            if ( splitValue )
            {
                StringTokenizer token = new StringTokenizer( value, "," );
                while ( token.hasMoreTokens() )
                {
                    String current = token.nextToken().trim();

                    if ( StringUtils.isNotEmpty( current ) )
                    {
                        arguments.add( current );

                        if ( token.hasMoreTokens() && repeatKey )
                        {
                            arguments.add( key );
                        }
                    }
                }
            }
            else
            {
                arguments.add( value );
            }
        }
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * if the the value is not null or empty.
     * <p/>
     * Moreover, the value could be comma separated.
     *
     * @param arguments a list of arguments, not null
     * @param key       the argument name.
     * @param value     the argument value to be added.
     * @param repeatKey repeat or not the key in the command line
     */
    private void addArgIfNotEmpty( List<String> arguments, String key, String value, boolean repeatKey )
    {
        addArgIfNotEmpty( arguments, key, value, repeatKey, true );
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * regarding the requested Java version.
     *
     * @param arguments           a list of arguments, not null
     * @param key                 the argument name.
     * @param value               the argument value to be added.
     * @param requiredJavaVersion the required Java version, for example 1.31f or 1.4f
     * @see #addArgIfNotEmpty(List, String, String, JavaVersion, boolean)
     */
    private void addArgIfNotEmpty( List<String> arguments, String key, String value,
                                   JavaVersion requiredJavaVersion )
    {
        addArgIfNotEmpty( arguments, key, value, requiredJavaVersion, false );
    }

    /**
     * Convenience method to add an argument to the <code>command line</code>
     * regarding the requested Java version.
     *
     * @param arguments           a list of arguments, not null
     * @param key                 the argument name.
     * @param value               the argument value to be added.
     * @param requiredJavaVersion the required Java version, for example 1.31f or 1.4f
     * @param repeatKey           repeat or not the key in the command line
     * @see #addArgIfNotEmpty(List, String, String)
     * @see #isJavaDocVersionAtLeast
     */
    private void addArgIfNotEmpty( List<String> arguments, String key, String value, JavaVersion requiredJavaVersion,
                                   boolean repeatKey )
    {
        if ( StringUtils.isNotEmpty( value ) )
        {
            if ( isJavaDocVersionAtLeast( requiredJavaVersion ) )
            {
                addArgIfNotEmpty( arguments, key, value, repeatKey );
            }
            else
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( key + " option is not supported on Java version < " + requiredJavaVersion );
                }
            }
        }
    }

    /**
     * Convenience method to process {@code offlineLinks} values as individual <code>-linkoffline</code>
     * javadoc options.
     * <br/>
     * If {@code detectOfflineLinks}, try to add javadoc apidocs according Maven conventions for all modules given
     * in the project.
     *
     * @param arguments a list of arguments, not null
     * @throws MavenReportException if any
     * @see #offlineLinks
     * @see #getModulesLinks()
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#package-list">package-list spec</a>
     */
    private void addLinkofflineArguments( List<String> arguments, Set<OfflineLink> offlineLinksList )
        throws MavenReportException
    {
        for ( OfflineLink offlineLink : offlineLinksList )
        {
            String url = offlineLink.getUrl();
            if ( StringUtils.isEmpty( url ) )
            {
                continue;
            }
            url = cleanUrl( url );

            String location = offlineLink.getLocation();
            if ( StringUtils.isEmpty( location ) )
            {
                continue;
            }
            if ( isValidJavadocLink( location, false ) )
            {
                addArgIfNotEmpty( arguments, "-linkoffline",
                                  JavadocUtil.quotedPathArgument( url ) + " " + JavadocUtil.quotedPathArgument(
                                      location ), true );
            }
        }
    }

    private Set<OfflineLink> getLinkofflines() throws MavenReportException
    {
        Set<OfflineLink> offlineLinksList = collectOfflineLinks();

        offlineLinksList.addAll( getModulesLinks() );

        return offlineLinksList;
    }

    /**
     * Convenience method to process {@link #links} values as individual <code>-link</code> javadoc options.
     * If {@code detectLinks}, try to add javadoc apidocs according Maven conventions for all dependencies given
     * in the project.
     * <br/>
     * According the Javadoc documentation, all defined link should have <code>${link}/package-list</code> fetchable.
     * <br/>
     * <b>Note</b>: when a link is not fetchable:
     * <ul>
     * <li>Javadoc 1.4 and less throw an exception</li>
     * <li>Javadoc 1.5 and more display a warning</li>
     * </ul>
     *
     * @param arguments a list of arguments, not null
     * @throws MavenReportException issue while generating report
     * @see #detectLinks
     * @see #getDependenciesLinks()
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#package-list">package-list spec</a>
     */
    private void addLinkArguments( List<String> arguments )
        throws MavenReportException
    {
        Set<String> links = collectLinks();

        for ( String link : links )
        {
            if ( StringUtils.isEmpty( link ) )
            {
                continue;
            }

            if ( isOffline && !link.startsWith( "file:" ) )
            {
                continue;
            }

            while ( link.endsWith( "/" ) )
            {
                link = link.substring( 0, link.lastIndexOf( "/" ) );
            }

            addArgIfNotEmpty( arguments, "-link", JavadocUtil.quotedPathArgument( link ), true, false );
        }
    }

    /**
     * Coppy all resources to the output directory
     *
     * @param javadocOutputDirectory not null
     * @throws MavenReportException if any
     * @see #copyDefaultStylesheet(File)
     * @see #copyJavadocResources(File)
     * @see #copyAdditionalJavadocResources(File)
     */
    private void copyAllResources( File javadocOutputDirectory )
        throws MavenReportException
    {
        // ----------------------------------------------------------------------
        // Copy default resources
        // ----------------------------------------------------------------------

        try
        {
            copyDefaultStylesheet( javadocOutputDirectory );
        }
        catch ( IOException e )
        {
            throw new MavenReportException( "Unable to copy default stylesheet: " + e.getMessage(), e );
        }

        // ----------------------------------------------------------------------
        // Copy javadoc resources
        // ----------------------------------------------------------------------

        if ( docfilessubdirs )
        {
            /*
             * Workaround since -docfilessubdirs doesn't seem to be used correctly by the javadoc tool
             * (see other note about -sourcepath). Take care of the -excludedocfilessubdir option.
             */
            try
            {
                copyJavadocResources( javadocOutputDirectory );
            }
            catch ( IOException e )
            {
                throw new MavenReportException( "Unable to copy javadoc resources: " + e.getMessage(), e );
            }
        }

        // ----------------------------------------------------------------------
        // Copy additional javadoc resources in artifacts
        // ----------------------------------------------------------------------

        copyAdditionalJavadocResources( javadocOutputDirectory );
    }

    /**
     * Copies the {@code DEFAULT_CSS_NAME} css file from the current class
     * loader to the <code>outputDirectory</code> only if {@code stylesheetfile} is empty and
     * {@code stylesheet} is equals to <code>maven</code>.
     *
     * @param anOutputDirectory the output directory
     * @throws java.io.IOException if any
     * @see #DEFAULT_CSS_NAME
     * @see JavadocUtil#copyResource(java.net.URL, java.io.File)
     */
    private void copyDefaultStylesheet( File anOutputDirectory )
        throws IOException
    {
        if ( StringUtils.isNotEmpty( stylesheetfile ) )
        {
            return;
        }

        if ( !stylesheet.equalsIgnoreCase( "maven" ) )
        {
            return;
        }

        URL url = getClass().getClassLoader().getResource( RESOURCE_CSS_DIR + "/" + DEFAULT_CSS_NAME );
        File outFile = new File( anOutputDirectory, DEFAULT_CSS_NAME );
        JavadocUtil.copyResource( url, outFile );
    }

    /**
     * Method that copy all <code>doc-files</code> directories from <code>javadocDirectory</code> of
     * the current project or of the projects in the reactor to the <code>outputDirectory</code>.
     *
     * @param anOutputDirectory the output directory
     * @throws java.io.IOException if any
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.2.html#docfiles">Reference
     *      Guide, Copies new "doc-files" directory for holding images and examples</a>
     * @see #docfilessubdirs
     */
    private void copyJavadocResources( File anOutputDirectory )
        throws IOException
    {
        if ( anOutputDirectory == null || !anOutputDirectory.exists() )
        {
            throw new IOException( "The outputDirectory " + anOutputDirectory + " doesn't exists." );
        }

        if ( includeDependencySources )
        {
            resolveDependencyBundles();
            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    File dir = bundle.getResourcesDirectory();
                    JavadocOptions options = bundle.getOptions();
                    if ( dir != null && dir.isDirectory() )
                    {
                        JavadocUtil.copyJavadocResources( anOutputDirectory, dir, options == null
                            ? null
                            : options.getExcludedDocfilesSubdirs() );
                    }
                }
            }
        }

        if ( getJavadocDirectory() != null )
        {
            JavadocUtil.copyJavadocResources( anOutputDirectory, getJavadocDirectory(), excludedocfilessubdir );
        }

        if ( isAggregator() )
        {
            for ( MavenProject subProject : getAggregatedProjects()  )
            {
                if ( subProject != project && getJavadocDirectory() != null )
                {
                    String javadocDirRelative =
                        PathUtils.toRelative( project.getBasedir(), getJavadocDirectory().getAbsolutePath() );
                    File javadocDir = new File( subProject.getBasedir(), javadocDirRelative );
                    JavadocUtil.copyJavadocResources( anOutputDirectory, javadocDir, excludedocfilessubdir );
                }
            }
        }
    }

    private synchronized void resolveDependencyBundles()
        throws IOException
    {
        if ( dependencyJavadocBundles == null )
        {
            dependencyJavadocBundles =
                resourceResolver.resolveDependencyJavadocBundles( getDependencySourceResolverConfig() );
            if ( dependencyJavadocBundles == null )
            {
                dependencyJavadocBundles = new ArrayList<>();
            }
        }
    }

    /**
     * Method that copy additional Javadoc resources from given artifacts.
     *
     * @param anOutputDirectory the output directory
     * @throws MavenReportException if any
     * @see #resourcesArtifacts
     */
    private void copyAdditionalJavadocResources( File anOutputDirectory )
        throws MavenReportException
    {
        Set<ResourcesArtifact> resourcesArtifacts = collectResourcesArtifacts();
        if ( isEmpty( resourcesArtifacts ) )
        {
            return;
        }

        UnArchiver unArchiver;
        try
        {
            unArchiver = archiverManager.getUnArchiver( "jar" );
        }
        catch ( NoSuchArchiverException e )
        {
            throw new MavenReportException(
                "Unable to extract resources artifact. " + "No archiver for 'jar' available.", e );
        }

        for ( ResourcesArtifact item : resourcesArtifacts )
        {
            Artifact artifact;
            try
            {
                artifact = createAndResolveArtifact( item );
            }
            catch ( ArtifactResolverException e )
            {
                throw new MavenReportException( "Unable to resolve artifact:" + item, e );
            }

            unArchiver.setSourceFile( artifact.getFile() );
            unArchiver.setDestDirectory( anOutputDirectory );
            // remove the META-INF directory from resource artifact
            IncludeExcludeFileSelector[] selectors =
                new IncludeExcludeFileSelector[]{ new IncludeExcludeFileSelector() };
            selectors[0].setExcludes( new String[]{ "META-INF/**" } );
            unArchiver.setFileSelectors( selectors );

            getLog().info( "Extracting contents of resources artifact: " + artifact.getArtifactId() );
            try
            {
                unArchiver.extract();
            }
            catch ( ArchiverException e )
            {
                throw new MavenReportException(
                    "Extraction of resources failed. Artifact that failed was: " + artifact.getArtifactId(), e );
            }
        }
    }

    /**
     * @param sourcePaths could be null
     * @return the list of package names for files in the sourcePaths
     */
    private List<String> getPackageNames( Map<Path, Collection<String>> sourcePaths )
    {
        List<String> returnList = new ArrayList<>();

        if ( !StringUtils.isEmpty( sourcepath ) )
        {
            return returnList;
        }

        for ( Entry<Path, Collection<String>> currentPathEntry : sourcePaths.entrySet() )
        {
            for ( String currentFile : currentPathEntry.getValue() )
            {
                /*
                 * Remove the miscellaneous files
                 * https://docs.oracle.com/javase/1.4.2/docs/tooldocs/solaris/javadoc.html#unprocessed
                 */
                if ( currentFile.contains( "doc-files" ) )
                {
                    continue;
                }

                int lastIndexOfSeparator = currentFile.lastIndexOf( "/" );
                if ( lastIndexOfSeparator != -1 )
                {
                    String packagename = currentFile.substring( 0, lastIndexOfSeparator ).replace( '/', '.' );

                    if ( !returnList.contains( packagename ) )
                    {
                        returnList.add( packagename );
                    }
                }
            }
        }

        return returnList;
    }

    /**
     * @param javadocModules     not null
     * @return a list of exported package names for files in allSourcePaths
     * @throws MavenReportException if any
     * @see #getFiles
     * @see #getSourcePaths()
     */
    private Collection<String> getPackageNamesRespectingJavaModules( Collection<JavadocModule> javadocModules )
            throws MavenReportException
    {
        if ( !StringUtils.isEmpty( sourcepath ) )
        {
            return Collections.emptyList();
        }

        Set<String> returnList = new LinkedHashSet<>();
        for ( JavadocModule javadocModule  : javadocModules )
        {
            Collection<Path> artifactSourcePaths = javadocModule.getSourcePaths();
            Set<String> exportedPackages = new HashSet<>();
            boolean exportAllPackages;
            ResolvePathResult resolvedPath = getResolvePathResult( javadocModule.getArtifactFile() );
            if ( resolvedPath != null && resolvedPath.getModuleNameSource() == ModuleNameSource.MODULEDESCRIPTOR )
            {
                Set<JavaModuleDescriptor.JavaExports> exports = resolvedPath.getModuleDescriptor().exports();
                if ( exports.isEmpty() )
                {
                    continue;
                }
                for ( JavaModuleDescriptor.JavaExports export : exports )
                {
                    exportedPackages.add( export.source() );
                }
                exportAllPackages = false;
            }
            else
            {
                exportAllPackages = true;
            }

            for ( Map.Entry<Path, Collection<String>> currentPathEntry : getFiles( artifactSourcePaths ).entrySet() )
            {
                for ( String currentFile : currentPathEntry.getValue() )
                {
                    /*
                     * Remove the miscellaneous files
                     * https://docs.oracle.com/javase/1.4.2/docs/tooldocs/solaris/javadoc.html#unprocessed
                     */
                    if ( currentFile.contains( "doc-files" ) )
                    {
                        continue;
                    }

                    int lastIndexOfSeparator = currentFile.lastIndexOf( '/' );
                    if ( lastIndexOfSeparator != -1 )
                    {
                        String packagename =
                            currentFile.substring( 0, lastIndexOfSeparator ).replace( '/', '.' );

                        if ( exportAllPackages || exportedPackages.contains( packagename ) )
                        {
                            returnList.add( packagename );
                        }
                    }
                }
            }
        }

        return returnList;
    }

    /**
     * @param sourcePaths could be null
     * @return a list files with unnamed package names for files in the sourcePaths
     */
    private List<String> getFilesWithUnnamedPackages( Map<Path, Collection<String>> sourcePaths )
    {
        List<String> returnList = new ArrayList<>();

        if ( !StringUtils.isEmpty( sourcepath ) )
        {
            return returnList;
        }

        for ( Entry<Path, Collection<String>> currentPathEntry : sourcePaths.entrySet() )
        {
            Path currentSourcePath = currentPathEntry.getKey();

            for ( String currentFile : currentPathEntry.getValue() )
            {
                /*
                 * Remove the miscellaneous files
                 * https://docs.oracle.com/javase/1.4.2/docs/tooldocs/solaris/javadoc.html#unprocessed
                 */
                if ( currentFile.contains( "doc-files" ) )
                {
                    continue;
                }

                if ( currentFile.indexOf( File.separatorChar ) == -1 )
                {
                    returnList.add( currentSourcePath.resolve( currentFile ).toAbsolutePath().toString() );
                }
            }
        }

        return returnList;
    }

    /**
     * Either return only the module descriptor or all sourcefiles per sourcepath
     * @param sourcePaths could be null
     * @return a list of files
     */
    private List<String> getSpecialFiles( Map<Path, Collection<String>> sourcePaths )
    {
        if ( !StringUtils.isEmpty( sourcepath ) )
        {
            return new ArrayList<>();
        }

        boolean containsModuleDescriptor = false;
        for ( Collection<String> sourcepathFiles : sourcePaths.values() )
        {
            containsModuleDescriptor = sourcepathFiles.contains( "module-info.java" );
            if ( containsModuleDescriptor )
            {
                break;
            }
        }

        if ( containsModuleDescriptor )
        {
            return getModuleSourcePathFiles( sourcePaths );
        }
        else
        {
            return getFilesWithUnnamedPackages( sourcePaths );
        }
    }

    private List<String> getModuleSourcePathFiles( Map<Path, Collection<String>> sourcePaths )
    {
        List<String> returnList = new ArrayList<>();

        for ( Entry<Path, Collection<String>> currentPathEntry : sourcePaths.entrySet() )
        {
            Path currentSourcePath = currentPathEntry.getKey();
            if ( currentPathEntry.getValue().contains( "module-info.java" ) )
            {
                returnList.add( currentSourcePath.resolve( "module-info.java" ).toAbsolutePath().toString() );
            }
            else
            {
                for ( String currentFile : currentPathEntry.getValue() )
                {
                    /*
                     * Remove the miscellaneous files
                     * https://docs.oracle.com/javase/1.4.2/docs/tooldocs/solaris/javadoc.html#unprocessed
                     */
                    if ( currentFile.contains( "doc-files" ) )
                    {
                        continue;
                    }

                    returnList.add( currentSourcePath.resolve( currentFile ).toAbsolutePath().toString() );
                }
            }
        }
        return returnList;
    }

    /**
     * Generate an <code>options</code> file for all options and arguments and add the <code>@options</code> in the
     * command line.
     *
     * @param cmd                    not null
     * @param arguments              not null
     * @param javadocOutputDirectory not null
     * @throws MavenReportException if any
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#argumentfiles">
     *      Reference Guide, Command line argument files</a>
     * @see #OPTIONS_FILE_NAME
     */
    private void addCommandLineOptions( Commandline cmd, List<String> arguments, File javadocOutputDirectory )
        throws MavenReportException
    {
        File optionsFile = new File( javadocOutputDirectory, OPTIONS_FILE_NAME );

        StringBuilder options = new StringBuilder();
        options.append( StringUtils.join( arguments.iterator(),
                                          SystemUtils.LINE_SEPARATOR ) );

        Charset outputFileEncoding;
        if ( JAVA_VERSION.isAtLeast( "9" ) && JAVA_VERSION.isBefore( "12" ) )
        {
            outputFileEncoding = StandardCharsets.UTF_8;
        }
        else
        {
            outputFileEncoding = Charset.defaultCharset();
        }
        try
        {
            Files.write( optionsFile.toPath(), Collections.singleton( options ), outputFileEncoding );
        }
        catch ( IOException e )
        {
            throw new MavenReportException(
                "Unable to write '" + optionsFile.getName() + "' temporary file for command execution", e );
        }

        cmd.createArg().setValue( "@" + OPTIONS_FILE_NAME );
    }

    /**
     * Generate a file called <code>argfile</code> (or <code>files</code>, depending the JDK) to hold files and add
     * the <code>@argfile</code> (or <code>@file</code>, depending the JDK) in the command line.
     *
     * @param cmd                    not null
     * @param javadocOutputDirectory not null
     * @param files                  not null
     * @throws MavenReportException if any
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#argumentfiles">
     *      Reference Guide, Command line argument files
     *      </a>
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/whatsnew-1.4.html#runningjavadoc">
     *      What s New in Javadoc 1.4
     *      </a>
     * @see #isJavaDocVersionAtLeast(JavaVersion)
     * @see #ARGFILE_FILE_NAME
     * @see #FILES_FILE_NAME
     */
    private void addCommandLineArgFile( Commandline cmd, File javadocOutputDirectory, List<String> files )
        throws MavenReportException
    {
        File argfileFile;
        if ( JAVA_VERSION.compareTo( SINCE_JAVADOC_1_4 ) >= 0 )
        {
            argfileFile = new File( javadocOutputDirectory, ARGFILE_FILE_NAME );
            cmd.createArg().setValue( "@" + ARGFILE_FILE_NAME );
        }
        else
        {
            argfileFile = new File( javadocOutputDirectory, FILES_FILE_NAME );
            cmd.createArg().setValue( "@" + FILES_FILE_NAME );
        }

        List<String> quotedFiles = new ArrayList<>( files.size() );
        for ( String file : files )
        {
            quotedFiles.add( JavadocUtil.quotedPathArgument( file ) );
        }

        Charset cs;
        if ( JavaVersion.JAVA_SPECIFICATION_VERSION.isAtLeast( "9" )
            && JavaVersion.JAVA_SPECIFICATION_VERSION.isBefore( "12" ) )
        {
            cs = StandardCharsets.UTF_8;
        }
        else
        {
            cs = Charset.defaultCharset();
        }

        try
        {
            Files.write( argfileFile.toPath(), quotedFiles, cs );
        }
        catch ( IOException e )
        {
            throw new MavenReportException(
                "Unable to write '" + argfileFile.getName() + "' temporary file for command execution", e );
        }
    }

    /**
     * Generate a file called <code>packages</code> to hold all package names and add the <code>@packages</code> in
     * the command line.
     *
     * @param cmd                    not null
     * @param javadocOutputDirectory not null
     * @param packageNames           not null
     * @throws MavenReportException if any
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#argumentfiles">
     *      Reference Guide, Command line argument files</a>
     * @see #PACKAGES_FILE_NAME
     */
    private void addCommandLinePackages( Commandline cmd, File javadocOutputDirectory, Collection<String> packageNames )
        throws MavenReportException
    {
        File packagesFile = new File( javadocOutputDirectory, PACKAGES_FILE_NAME );

        try
        {
            FileUtils.fileWrite( packagesFile.getAbsolutePath(), null /* platform encoding */,
                                 StringUtils.join( packageNames.iterator(), SystemUtils.LINE_SEPARATOR ) );
        }
        catch ( IOException e )
        {
            throw new MavenReportException(
                "Unable to write '" + packagesFile.getName() + "' temporary file for command execution", e );
        }

        cmd.createArg().setValue( "@" + PACKAGES_FILE_NAME );
    }

    /**
     * Checks for the validity of the Javadoc options used by the user.
     *
     * @throws MavenReportException if error
     */
    private void validateJavadocOptions()
        throws MavenReportException
    {
        // encoding
        if ( StringUtils.isNotEmpty( getEncoding() ) && !JavadocUtil.validateEncoding( getEncoding() ) )
        {
            throw new MavenReportException( "Unsupported option <encoding/> '" + getEncoding() + "'" );
        }

        // locale
        if ( StringUtils.isNotEmpty( this.locale ) )
        {
            StringTokenizer tokenizer = new StringTokenizer( this.locale, "_" );
            final int maxTokens = 3;
            if ( tokenizer.countTokens() > maxTokens )
            {
                throw new MavenReportException(
                    "Unsupported option <locale/> '" + this.locale + "', should be language_country_variant." );
            }

            Locale localeObject = null;
            if ( tokenizer.hasMoreTokens() )
            {
                String language = tokenizer.nextToken().toLowerCase( Locale.ENGLISH );
                if ( !Arrays.asList( Locale.getISOLanguages() ).contains( language ) )
                {
                    throw new MavenReportException(
                        "Unsupported language '" + language + "' in option <locale/> '" + this.locale + "'" );
                }
                localeObject = new Locale( language );

                if ( tokenizer.hasMoreTokens() )
                {
                    String country = tokenizer.nextToken().toUpperCase( Locale.ENGLISH );
                    if ( !Arrays.asList( Locale.getISOCountries() ).contains( country ) )
                    {
                        throw new MavenReportException(
                            "Unsupported country '" + country + "' in option <locale/> '" + this.locale + "'" );
                    }
                    localeObject = new Locale( language, country );

                    if ( tokenizer.hasMoreTokens() )
                    {
                        String variant = tokenizer.nextToken();
                        localeObject = new Locale( language, country, variant );
                    }
                }
            }

            if ( localeObject == null )
            {
                throw new MavenReportException(
                    "Unsupported option <locale/> '" + this.locale + "', should be language_country_variant." );
            }

            this.locale = localeObject.toString();
            final List<Locale> availableLocalesList = Arrays.asList( Locale.getAvailableLocales() );
            if ( StringUtils.isNotEmpty( localeObject.getVariant() ) && !availableLocalesList.contains( localeObject ) )
            {
                StringBuilder sb = new StringBuilder();
                sb.append( "Unsupported option <locale/> with variant '" ).append( this.locale );
                sb.append( "'" );

                localeObject = new Locale( localeObject.getLanguage(), localeObject.getCountry() );
                this.locale = localeObject.toString();

                sb.append( ", trying to use <locale/> without variant, i.e. '" ).append( this.locale ).append( "'" );
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( sb.toString() );
                }
            }

            if ( !availableLocalesList.contains( localeObject ) )
            {
                throw new MavenReportException( "Unsupported option <locale/> '" + this.locale + "'" );
            }
        }
    }

    /**
     * Checks for the validity of the Standard Doclet options.
     * <br/>
     * For example, throw an exception if &lt;nohelp/&gt; and &lt;helpfile/&gt; options are used together.
     *
     * @throws MavenReportException if error or conflict found
     */
    private void validateStandardDocletOptions()
        throws MavenReportException
    {
        // docencoding
        if ( StringUtils.isNotEmpty( getDocencoding() ) && !JavadocUtil.validateEncoding( getDocencoding() ) )
        {
            throw new MavenReportException( "Unsupported option <docencoding/> '" + getDocencoding() + "'" );
        }

        // charset
        if ( StringUtils.isNotEmpty( getCharset() ) && !JavadocUtil.validateEncoding( getCharset() ) )
        {
            throw new MavenReportException( "Unsupported option <charset/> '" + getCharset() + "'" );
        }

        // helpfile
        if ( StringUtils.isNotEmpty( helpfile ) && nohelp )
        {
            throw new MavenReportException( "Option <nohelp/> conflicts with <helpfile/>" );
        }

        // overview
        if ( getOverview() != null && getOverview().exists() && nooverview )
        {
            throw new MavenReportException( "Option <nooverview/> conflicts with <overview/>" );
        }

        // index
        if ( splitindex && noindex )
        {
            throw new MavenReportException( "Option <noindex/> conflicts with <splitindex/>" );
        }

        // stylesheet
        if ( StringUtils.isNotEmpty( stylesheet ) && !( stylesheet.equalsIgnoreCase( "maven" )
            || stylesheet.equalsIgnoreCase( "java" ) ) )
        {
            throw new MavenReportException( "Option <stylesheet/> supports only \"maven\" or \"java\" value." );
        }
    }

    /**
     * Add Standard Javadoc Options.
     * <br/>
     * The <a href="package-summary.html#Standard_Javadoc_Options">package documentation</a> details the
     * Standard Javadoc Options wrapped by this Plugin.
     *
     * @param javadocOutputDirectory not null
     * @param arguments              not null
     * @param allSourcePaths         not null
     * @throws MavenReportException if any
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#javadocoptions">https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#javadocoptions</a>
     */
    private void addJavadocOptions( File javadocOutputDirectory,
                                    List<String> arguments,
                                    Collection<JavadocModule> allSourcePaths,
                                    Set<OfflineLink> offlineLinks )
        throws MavenReportException
    {
        Collection<Path> sourcePaths = allSourcePaths.stream()
                                                     .flatMap( e -> e.getSourcePaths().stream() )
                                                     .collect( Collectors.toList() );

        validateJavadocOptions();

        // see com.sun.tools.javadoc.Start#parseAndExecute(String argv[])
        addArgIfNotEmpty( arguments, "-locale", JavadocUtil.quotedArgument( this.locale ) );

        // all options in alphabetical order

        if ( old && isJavaDocVersionAtLeast( SINCE_JAVADOC_1_4 ) )
        {
            if ( getLog().isWarnEnabled() )
            {
                getLog().warn( "Javadoc 1.4+ doesn't support the -1.1 switch anymore. Ignore this option." );
            }
        }
        else
        {
            addArgIf( arguments, old, "-1.1" );
        }

        addArgIfNotEmpty( arguments, "-bootclasspath", JavadocUtil.quotedPathArgument( getBootclassPath() ) );

        if ( isJavaDocVersionAtLeast( SINCE_JAVADOC_1_5 ) )
        {
            addArgIf( arguments, breakiterator, "-breakiterator", SINCE_JAVADOC_1_5 );
        }

        List<MavenProject> aggregatedProjects = reactorProjects; // getAggregatedProjects();
        Map<String, MavenProject> reactorKeys = new HashMap<>( aggregatedProjects.size() );
        for ( MavenProject reactorProject : aggregatedProjects )
        {
            reactorKeys.put( ArtifactUtils.versionlessKey( reactorProject.getGroupId(),
                                                           reactorProject.getArtifactId() ), reactorProject );
        }

        Map<String, JavaModuleDescriptor> allModuleDescriptors = new HashMap<>();

        boolean supportModulePath = javadocRuntimeVersion.isAtLeast( "9" );
        if ( release != null )
        {
            supportModulePath &= JavaVersion.parse( release ).isAtLeast( "9" );
        }
        else if ( source != null )
        {
            supportModulePath &= JavaVersion.parse( source ).isAtLeast( "9" );
        }

        if ( supportModulePath )
        {
            for ( JavadocModule entry : allSourcePaths )
            {
                if ( entry.getModuleNameSource() == null || entry.getModuleNameSource() == ModuleNameSource.FILENAME )
                {
                    Path moduleDescriptor = findMainDescriptor( entry.getSourcePaths() );

                    if ( moduleDescriptor != null )
                    {
                        try
                        {
                            allModuleDescriptors.put( entry.getGa(),
                                      locationManager.parseModuleDescriptor( moduleDescriptor ).getModuleDescriptor() );
                        }
                        catch ( IOException e )
                        {
                            throw new MavenReportException( e.getMessage(), e );
                        }
                    }
                }
                else
                {
                    allModuleDescriptors.put( entry.getGa(), entry.getModuleDescriptor() );
                }
            }
        }

        Collection<String> additionalModules = new ArrayList<>();

        ResolvePathResult mainResolvePathResult = null;

        Map<String, Collection<Path>> patchModules = new HashMap<>();

        Path moduleSourceDir = null;
        if ( supportModulePath && !allModuleDescriptors.isEmpty() )
        {
            Collection<String> unnamedProjects = new ArrayList<>();
            for ( JavadocModule javadocModule : allSourcePaths )
            {
                MavenProject aggregatedProject = reactorKeys.get( javadocModule.getGa() );
                if ( aggregatedProject != null && !"pom".equals( aggregatedProject.getPackaging() ) )
                {
                    ResolvePathResult result = null;

                    // Prefer jar over outputDirectory, since it may may contain an automatic module name
                    File artifactFile = getClassesFile( aggregatedProject );
                    if ( artifactFile != null )
                    {
                        ResolvePathRequest<File> request = ResolvePathRequest.ofFile( artifactFile );
                        try
                        {
                            result = locationManager.resolvePath( request );
                        }
                        catch ( RuntimeException e )
                        {
                            // most likely an invalid module name based on filename
                            if ( !"java.lang.module.FindException".equals( e.getClass().getName() ) )
                            {
                                throw e;
                            }
                        }
                        catch ( IOException e )
                        {
                            throw new MavenReportException( e.getMessage(), e );
                        }
                    }
                    else
                    {
                        Path moduleDescriptor = findMainDescriptor( javadocModule.getSourcePaths() );

                        if ( moduleDescriptor != null )
                        {
                            try
                            {
                                result = locationManager.parseModuleDescriptor( moduleDescriptor );
                            }
                            catch ( IOException e )
                            {
                                throw new MavenReportException( e.getMessage(), e );
                            }
                        }
                    }

                    if ( result != null && result.getModuleDescriptor() != null )
                    {
                        moduleSourceDir = javadocOutputDirectory.toPath().resolve( "src" );
                        try
                        {
                            moduleSourceDir = Files.createDirectories( moduleSourceDir );

                            additionalModules.add( result.getModuleDescriptor().name() );

                            patchModules.put( result.getModuleDescriptor().name(), javadocModule.getSourcePaths() );

                            Path modulePath = moduleSourceDir.resolve( result.getModuleDescriptor().name() );
                            if ( !Files.isDirectory( modulePath ) )
                            {
                                Files.createDirectory( modulePath );
                            }
                        }
                        catch ( IOException e )
                        {
                            throw new MavenReportException( e.getMessage(), e );
                        }
                    }
                    else
                    {
                        unnamedProjects.add( javadocModule.getGa() );
                    }

                    if ( aggregatedProject.equals( getProject() ) )
                    {
                        mainResolvePathResult = result;
                    }
                }
                else
                {
                    // todo
                    getLog().error( "no reactor project: " + javadocModule.getGa() );
                }
            }

            if ( !unnamedProjects.isEmpty() )
            {
                getLog().error( "Creating an aggregated report for both named and unnamed modules is not possible." );
                getLog().error( "Ensure that every module has a module descriptor or is a jar with a MANIFEST.MF "
                    + "containing an Automatic-Module-Name." );
                getLog().error( "Fix the following projects:" );
                for ( String unnamedProject : unnamedProjects )
                {
                    getLog().error( " - " + unnamedProject );
                }
                throw new MavenReportException( "Aggregator report contains named and unnamed modules" );
            }

            if ( mainResolvePathResult != null
                && ModuleNameSource.MANIFEST.equals( mainResolvePathResult.getModuleNameSource() ) )
            {
                arguments.add( "--add-modules" );
                arguments.add( "ALL-MODULE-PATH" );
            }
        }

        // MJAVADOC-506
        boolean moduleDescriptorSource = false;
        for ( Path sourcepath : sourcePaths )
        {
            if ( Files.isRegularFile( sourcepath.resolve( "module-info.java" ) ) )
            {
                moduleDescriptorSource = true;
                break;
            }
        }

        final ModuleNameSource mainModuleNameSource;
        if ( mainResolvePathResult != null )
        {
            mainModuleNameSource = mainResolvePathResult.getModuleNameSource();
        }
        else
        {
            mainModuleNameSource = null;
        }

        if ( supportModulePath
             && ( isAggregator()
                  || ModuleNameSource.MODULEDESCRIPTOR.equals( mainModuleNameSource )
                  || ModuleNameSource.MANIFEST.equals( mainModuleNameSource ) ) )
        {
            List<File> pathElements = new ArrayList<>( getPathElements() );
            File artifactFile = getClassesFile( project );
            if ( artifactFile != null )
            {
                pathElements.add( 0, artifactFile );
            }

            ResolvePathsRequest<File> request =
                ResolvePathsRequest.ofFiles( pathElements );

            String mainModuleName = null;
            if ( mainResolvePathResult != null )
            {
                request.setModuleDescriptor( mainResolvePathResult.getModuleDescriptor() );
                mainModuleName = mainResolvePathResult.getModuleDescriptor().name();
            }

            request.setAdditionalModules( additionalModules );
            request.setIncludeStatic( isAggregator() );

            try
            {
                ResolvePathsResult<File> result = locationManager.resolvePaths( request );

                Set<File> modulePathElements = new HashSet<>( result.getModulepathElements().keySet() )  ;

                Collection<File> classPathElements = new ArrayList<>( result.getClasspathElements().size() );

                for ( File file : result.getClasspathElements() )
                {
                    if ( file.isDirectory() && new File( file, "module-info.class" ).exists() )
                    {
                        modulePathElements.add( file );
                    }
                    else if ( ModuleNameSource.MANIFEST.equals( mainModuleNameSource ) )
                    {
                        ModuleNameSource depModuleNameSource =
                            locationManager.resolvePath( ResolvePathRequest.ofFile( file ) ).getModuleNameSource();
                        if ( ModuleNameSource.MODULEDESCRIPTOR.equals( depModuleNameSource )
                            || ModuleNameSource.MANIFEST.equals( depModuleNameSource ) )
                        {
                            modulePathElements.add( file );
                        }
                        else
                        {
                            patchModules.get( mainModuleName ).add( file.toPath() );
                        }
                    }
                    else
                    {
                        classPathElements.add( file );
                    }
                }

                /* MJAVADOC-620: also add all JARs where module-name-guessing leads to a FindException: */
                for ( Entry<File, Exception> pathExceptionEntry : result.getPathExceptions().entrySet() )
                {
                    Exception exception = pathExceptionEntry.getValue();
                    // For Java < 9 compatibility, reference FindException by name:
                    if ( "java.lang.module.FindException".equals( exception.getClass().getName() ) )
                    {
                        File jarPath = pathExceptionEntry.getKey();
                        classPathElements.add( jarPath );
                    }
                }

                String classpath = StringUtils.join( classPathElements.iterator(), File.pathSeparator );
                addArgIfNotEmpty( arguments, "--class-path", JavadocUtil.quotedPathArgument( classpath ), false,
                                  false );

                String modulepath =
                    StringUtils.join( modulePathElements.iterator(), File.pathSeparator );
                addArgIfNotEmpty( arguments, "--module-path", JavadocUtil.quotedPathArgument( modulepath ), false,
                                  false );
            }
            catch ( IOException e )
            {
                throw new MavenReportException( e.getMessage(), e );
            }
        }
        else if ( supportModulePath && moduleDescriptorSource && !isTest() )
        {
            String modulepath = StringUtils.join( getPathElements().iterator(), File.pathSeparator );
            addArgIfNotEmpty( arguments, "--module-path", JavadocUtil.quotedPathArgument( modulepath ) , false, false );
        }
        else
        {
            String classpath = StringUtils.join( getPathElements().iterator(), File.pathSeparator );
            addArgIfNotEmpty( arguments, "-classpath", JavadocUtil.quotedPathArgument( classpath ) , false, false );
        }

        for ( Entry<String, Collection<Path>> entry : patchModules.entrySet() )
        {
            addArgIfNotEmpty( arguments, "--patch-module", entry.getKey() + '='
                              + JavadocUtil.quotedPathArgument( getSourcePath( entry.getValue() ) ),
                                false, false );
        }

        if ( StringUtils.isNotEmpty( doclet ) )
        {
            addArgIfNotEmpty( arguments, "-doclet", JavadocUtil.quotedArgument( doclet ) );
            addArgIfNotEmpty( arguments, "-docletpath", JavadocUtil.quotedPathArgument( getDocletPath() ) );
        }

        if ( StringUtils.isEmpty( encoding ) )
        {
            getLog().warn(
                "Source files encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
                    + ", i.e. build is platform dependent!" );
        }
        addArgIfNotEmpty( arguments, "-encoding", JavadocUtil.quotedArgument( getEncoding() ) );

        addArgIfNotEmpty( arguments, "-extdirs",
                          JavadocUtil.quotedPathArgument( JavadocUtil.unifyPathSeparator( extdirs ) ) );

        if ( ( getOverview() != null ) && ( getOverview().exists() ) )
        {
            addArgIfNotEmpty( arguments, "-overview",
                              JavadocUtil.quotedPathArgument( getOverview().getAbsolutePath() ) );
        }

        arguments.add( getAccessLevel() );

        if ( isJavaDocVersionAtLeast( SINCE_JAVADOC_1_5 ) )
        {
            addArgIf( arguments, quiet, "-quiet", SINCE_JAVADOC_1_5 );
        }

        if ( release != null )
        {
            arguments.add( "--release" );
            arguments.add( release );
        }
        else
        {
            addArgIfNotEmpty( arguments, "-source", JavadocUtil.quotedArgument( source ), SINCE_JAVADOC_1_4 );
        }

        if ( ( StringUtils.isEmpty( sourcepath ) ) && ( StringUtils.isNotEmpty( subpackages ) ) )
        {
            sourcepath = StringUtils.join( sourcePaths.iterator(), File.pathSeparator );
        }

        if ( moduleSourceDir == null )
        {
            addArgIfNotEmpty( arguments, "-sourcepath",
                              JavadocUtil.quotedPathArgument( getSourcePath( sourcePaths ) ), false, false );
        }
        else if ( mainResolvePathResult == null
            || ModuleNameSource.MODULEDESCRIPTOR.equals( mainResolvePathResult.getModuleNameSource() ) )
        {
            addArgIfNotEmpty( arguments, "--module-source-path",
                              JavadocUtil.quotedPathArgument( moduleSourceDir.toString() ) );
        }


        if ( StringUtils.isNotEmpty( sourcepath ) && isJavaDocVersionAtLeast( SINCE_JAVADOC_1_5 ) )
        {
            addArgIfNotEmpty( arguments, "-subpackages", subpackages, SINCE_JAVADOC_1_5 );
        }

        // [MJAVADOC-497] must be after sourcepath is recalculated, since getExcludedPackages() depends on it
        addArgIfNotEmpty( arguments, "-exclude", getExcludedPackages( sourcePaths ), SINCE_JAVADOC_1_4 );

        addArgIf( arguments, verbose, "-verbose" );

        if ( additionalOptions != null && additionalOptions.length > 0 )
        {
            for ( String additionalOption : additionalOptions )
            {
                arguments.add( additionalOption.replaceAll( "(?<!\\\\)\\\\(?!\\\\|:)", "\\\\" ) );
            }
        }
    }

    private ResolvePathResult getResolvePathResult( File artifactFile )
    {
        if ( artifactFile == null )
        {
            return null;
        }

        ResolvePathResult resolvePathResult = null;
        ResolvePathRequest<File> resolvePathRequest = ResolvePathRequest.ofFile( artifactFile );
        try
        {
            resolvePathResult = locationManager.resolvePath( resolvePathRequest );

            // happens when artifactFile is a directory without module descriptor
            if ( resolvePathResult.getModuleDescriptor() == null )
            {
                return null;
            }
        }
        catch ( IOException | RuntimeException /* e.g java.lang.module.FindException */ e )
        {
            if ( getLog().isDebugEnabled() )
            {
                Throwable cause = e;
                while ( cause.getCause() != null )
                {
                    cause = cause.getCause();
                }

                getLog().debug( "resolve path for: " + artifactFile + " cause error: " + cause );
            }
        }
        return resolvePathResult;
    }

    private Path findMainDescriptor( Collection<Path> roots ) throws MavenReportException
    {
        for ( Map.Entry<Path, Collection<String>> entry : getFiles( roots ).entrySet() )
        {
            if ( entry.getValue().contains( "module-info.java" ) )
            {
                return entry.getKey().resolve( "module-info.java" );
            }
        }
        return null;
    }

    /**
     * Add Standard Doclet Options.
     * <br/>
     * The <a href="package-summary.html#Standard_Doclet_Options">package documentation</a> details the
     * Standard Doclet Options wrapped by this Plugin.
     *
     * @param javadocOutputDirectory not null
     * @param arguments              not null
     * @throws MavenReportException if any
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#standard">
     *      https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#standard</a>
     */
    private void addStandardDocletOptions( File javadocOutputDirectory,
                                           List<String> arguments,
                                           Set<OfflineLink> offlineLinks )
        throws MavenReportException
    {
        validateStandardDocletOptions();

        // all options in alphabetical order

        addArgIf( arguments, author, "-author" );

        addArgIfNotEmpty( arguments, "-bottom", JavadocUtil.quotedArgument( getBottomText() ), false, false );

        if ( !isJavaDocVersionAtLeast( SINCE_JAVADOC_1_5 ) )
        {
            addArgIf( arguments, breakiterator, "-breakiterator", SINCE_JAVADOC_1_4 );
        }

        addArgIfNotEmpty( arguments, "-charset", JavadocUtil.quotedArgument( getCharset() ) );

        addArgIfNotEmpty( arguments, "-d", JavadocUtil.quotedPathArgument( javadocOutputDirectory.toString() ) );

        addArgIfNotEmpty( arguments, "-docencoding", JavadocUtil.quotedArgument( getDocencoding() ) );

        addArgIf( arguments, docfilessubdirs, "-docfilessubdirs", SINCE_JAVADOC_1_4 );

        addArgIf( arguments, StringUtils.isNotEmpty( doclint ), "-Xdoclint:" + getDoclint(), SINCE_JAVADOC_1_8 );

        addArgIfNotEmpty( arguments, "-doctitle", JavadocUtil.quotedArgument( getDoctitle() ), false, false );

        if ( docfilessubdirs )
        {
            addArgIfNotEmpty( arguments, "-excludedocfilessubdir",
                              JavadocUtil.quotedPathArgument( excludedocfilessubdir ), SINCE_JAVADOC_1_4 );
        }

        addArgIfNotEmpty( arguments, "-footer", JavadocUtil.quotedArgument( footer ), false, false );

        addGroups( arguments );

        addArgIfNotEmpty( arguments, "-header", JavadocUtil.quotedArgument( header ), false, false );

        Optional<File> helpFile = getHelpFile( javadocOutputDirectory );
        if ( helpFile.isPresent() )
        {
            addArgIfNotEmpty( arguments, "-helpfile",
                              JavadocUtil.quotedPathArgument( helpFile.get().getAbsolutePath() ) );
        }

        addArgIf( arguments, keywords, "-keywords", SINCE_JAVADOC_1_4_2 );

        addLinkArguments( arguments );

        addLinkofflineArguments( arguments, offlineLinks );

        addArgIf( arguments, linksource, "-linksource", SINCE_JAVADOC_1_4 );

        if ( sourcetab > 0 )
        {
            if ( javadocRuntimeVersion == SINCE_JAVADOC_1_4_2 )
            {
                addArgIfNotEmpty( arguments, "-linksourcetab", String.valueOf( sourcetab ) );
            }
            addArgIfNotEmpty( arguments, "-sourcetab", String.valueOf( sourcetab ), SINCE_JAVADOC_1_5 );
        }

        addArgIf( arguments, nocomment, "-nocomment", SINCE_JAVADOC_1_4 );

        addArgIf( arguments, nodeprecated, "-nodeprecated" );

        addArgIf( arguments, nodeprecatedlist, "-nodeprecatedlist" );

        addArgIf( arguments, nohelp, "-nohelp" );

        addArgIf( arguments, noindex, "-noindex" );

        addArgIf( arguments, nonavbar, "-nonavbar" );

        addArgIf( arguments, nooverview, "-nooverview" );

        addArgIfNotEmpty( arguments, "-noqualifier", JavadocUtil.quotedArgument( noqualifier ), SINCE_JAVADOC_1_4 );

        addArgIf( arguments, nosince, "-nosince" );

        if ( !notimestamp && MavenArchiver.parseBuildOutputTimestamp( outputTimestamp ).isPresent() )
        {
            // Override the notimestamp option if a Reproducible Build is requested.
            notimestamp = true;
        }

        addArgIf( arguments, notimestamp, "-notimestamp", SINCE_JAVADOC_1_5 );

        addArgIf( arguments, notree, "-notree" );

        addArgIfNotEmpty( arguments, "-packagesheader", JavadocUtil.quotedArgument( packagesheader ),
                          SINCE_JAVADOC_1_4_2 );

        if ( !isJavaDocVersionAtLeast( SINCE_JAVADOC_1_5 ) ) // Sun bug: 4714350
        {
            addArgIf( arguments, quiet, "-quiet", SINCE_JAVADOC_1_4 );
        }

        addArgIf( arguments, serialwarn, "-serialwarn" );

        addArgIf( arguments, splitindex, "-splitindex" );

        Optional<File> stylesheetfile = getStylesheetFile( javadocOutputDirectory );

        if ( stylesheetfile.isPresent() )
        {
            addArgIfNotEmpty( arguments, "-stylesheetfile",
                              JavadocUtil.quotedPathArgument( stylesheetfile.get().getAbsolutePath() ) );
        }

        addAddStyleSheets( arguments );

        if ( StringUtils.isNotEmpty( sourcepath ) && !isJavaDocVersionAtLeast( SINCE_JAVADOC_1_5 ) )
        {
            addArgIfNotEmpty( arguments, "-subpackages", subpackages, SINCE_JAVADOC_1_4 );
        }

        addArgIfNotEmpty( arguments, "-taglet", JavadocUtil.quotedArgument( taglet ), SINCE_JAVADOC_1_4 );
        addTaglets( arguments );
        addTagletsFromTagletArtifacts( arguments );
        addArgIfNotEmpty( arguments, "-tagletpath", JavadocUtil.quotedPathArgument( getTagletPath() ),
                          SINCE_JAVADOC_1_4 );

        addTags( arguments );

        addArgIfNotEmpty( arguments, "-top", JavadocUtil.quotedArgument( top ), false, false, SINCE_JAVADOC_1_6 );

        addArgIf( arguments, use, "-use" );

        addArgIf( arguments, version, "-version" );

        addArgIfNotEmpty( arguments, "-windowtitle", JavadocUtil.quotedArgument( getWindowtitle() ), false, false );
    }

    /**
     * Add <code>groups</code> parameter to arguments.
     *
     * @param arguments not null
     * @throws MavenReportException
     */
    private void addGroups( List<String> arguments )
        throws MavenReportException
    {
        Set<Group> groups = collectGroups();
        if ( isEmpty( groups ) )
        {
            return;
        }

        for ( Group group : groups )
        {
            if ( group == null || StringUtils.isEmpty( group.getTitle() ) || StringUtils.isEmpty(
                group.getPackages() ) )
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( "A group option is empty. Ignore this option." );
                }
            }
            else
            {
                String groupTitle = StringUtils.replace( group.getTitle(), ",", "&#44;" );
                addArgIfNotEmpty( arguments, "-group",
                                  JavadocUtil.quotedArgument( groupTitle ) + " " + JavadocUtil.quotedArgument(
                                      group.getPackages() ), true );
            }
        }
    }

    /**
     * Add <code>tags</code> parameter to arguments.
     *
     * @param arguments not null
     * @throws MavenReportException
     */
    private void addTags( List<String> arguments )
        throws MavenReportException
    {
        final String lineSeparator;
        if ( javadocRuntimeVersion.isBefore( "9" ) )
        {
            lineSeparator = " ";
        }
        else
        {
            lineSeparator = " \\\\" + SystemUtils.LINE_SEPARATOR;
        }

        for ( Tag tag : collectTags() )
        {
            if ( StringUtils.isEmpty( tag.getName() ) )
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( "A tag name is empty. Ignore this option." );
                }
            }
            else
            {
                String value = "\"" + tag.getName();
                if ( StringUtils.isNotEmpty( tag.getPlacement() ) )
                {
                    value += ":" + tag.getPlacement().replaceAll( "\\R", lineSeparator );
                    if ( StringUtils.isNotEmpty( tag.getHead() ) )
                    {
                        value += ":" + tag.getHead().replaceAll( "\\R", lineSeparator );
                    }
                }
                value += "\"";
                addArgIfNotEmpty( arguments, "-tag", value, SINCE_JAVADOC_1_4 );
            }
        }
    }

    /**
     * Add <code>taglets</code> parameter to arguments.
     *
     * @param arguments not null
     */
    private void addTaglets( List<String> arguments )
    {
        if ( taglets == null )
        {
            return;
        }

        for ( Taglet taglet1 : taglets )
        {
            if ( ( taglet1 == null ) || ( StringUtils.isEmpty( taglet1.getTagletClass() ) ) )
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( "A taglet option is empty. Ignore this option." );
                }
            }
            else
            {
                addArgIfNotEmpty( arguments, "-taglet", JavadocUtil.quotedArgument( taglet1.getTagletClass() ),
                                  SINCE_JAVADOC_1_4 );
            }
        }
    }

    /**
     * Auto-detect taglets class name from <code>tagletArtifacts</code> and add them to arguments.
     *
     * @param arguments not null
     * @throws MavenReportException if any
     * @see JavadocUtil#getTagletClassNames(File)
     */
    private void addTagletsFromTagletArtifacts( List<String> arguments )
        throws MavenReportException
    {
        Set<TagletArtifact> tArtifacts = new LinkedHashSet<>();
        if ( tagletArtifacts != null && tagletArtifacts.length > 0 )
        {
            tArtifacts.addAll( Arrays.asList( tagletArtifacts ) );
        }

        if ( includeDependencySources )
        {
            try
            {
                resolveDependencyBundles();
            }
            catch ( IOException e )
            {
                throw new MavenReportException(
                    "Failed to resolve javadoc bundles from dependencies: " + e.getMessage(), e );
            }

            if ( isNotEmpty( dependencyJavadocBundles ) )
            {
                for ( JavadocBundle bundle : dependencyJavadocBundles )
                {
                    JavadocOptions options = bundle.getOptions();
                    if ( options != null && isNotEmpty( options.getTagletArtifacts() ) )
                    {
                        tArtifacts.addAll( options.getTagletArtifacts() );
                    }
                }
            }
        }

        if ( isEmpty( tArtifacts ) )
        {
            return;
        }

        List<String> tagletsPath = new ArrayList<>();

        for ( TagletArtifact aTagletArtifact : tArtifacts )
        {
            if ( ( StringUtils.isNotEmpty( aTagletArtifact.getGroupId() ) ) && ( StringUtils.isNotEmpty(
                aTagletArtifact.getArtifactId() ) ) && ( StringUtils.isNotEmpty( aTagletArtifact.getVersion() ) ) )
            {
                Artifact artifact;
                try
                {
                    artifact = createAndResolveArtifact( aTagletArtifact );
                }
                catch ( ArtifactResolverException e )
                {
                    throw new MavenReportException( "Unable to resolve artifact:" + aTagletArtifact, e );
                }

                tagletsPath.add( artifact.getFile().getAbsolutePath() );
            }
        }

        tagletsPath = JavadocUtil.pruneFiles( tagletsPath );

        for ( String tagletJar : tagletsPath )
        {
            if ( !tagletJar.toLowerCase( Locale.ENGLISH ).endsWith( ".jar" ) )
            {
                continue;
            }

            List<String> tagletClasses;
            try
            {
                tagletClasses = JavadocUtil.getTagletClassNames( new File( tagletJar ) );
            }
            catch ( IOException e )
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( "Unable to auto-detect Taglet class names from '" + tagletJar
                                       + "'. Try to specify them with <taglets/>." );
                }
                if ( getLog().isDebugEnabled() )
                {
                    getLog().debug( "IOException: " + e.getMessage(), e );
                }
                continue;
            }
            catch ( ClassNotFoundException e )
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( "Unable to auto-detect Taglet class names from '" + tagletJar
                                       + "'. Try to specify them with <taglets/>." );
                }
                if ( getLog().isDebugEnabled() )
                {
                    getLog().debug( "ClassNotFoundException: " + e.getMessage(), e );
                }
                continue;
            }
            catch ( NoClassDefFoundError e )
            {
                if ( getLog().isWarnEnabled() )
                {
                    getLog().warn( "Unable to auto-detect Taglet class names from '" + tagletJar
                                       + "'. Try to specify them with <taglets/>." );
                }
                if ( getLog().isDebugEnabled() )
                {
                    getLog().debug( "NoClassDefFoundError: " + e.getMessage(), e );
                }
                continue;
            }

            if ( tagletClasses != null && !tagletClasses.isEmpty() )
            {
                for ( String tagletClass : tagletClasses )
                {
                    addArgIfNotEmpty( arguments, "-taglet", JavadocUtil.quotedArgument( tagletClass ),
                                      SINCE_JAVADOC_1_4 );
                }
            }
        }
    }

    /**
     * Execute the Javadoc command line
     *
     * @param cmd                    not null
     * @param javadocOutputDirectory not null
     * @throws MavenReportException if any errors occur
     */
    private void executeJavadocCommandLine( Commandline cmd, File javadocOutputDirectory )
            throws MavenReportException
    {
        if ( staleDataPath != null )
        {
            if ( !isUpToDate( cmd ) )
            {
                doExecuteJavadocCommandLine( cmd, javadocOutputDirectory );
                StaleHelper.writeStaleData( cmd, staleDataPath.toPath() );
            }
        }
        else
        {
            doExecuteJavadocCommandLine( cmd, javadocOutputDirectory );
        }
    }

    /**
     * Check if the javadoc is uptodate or not
     *
     * @param cmd                    not null
     * @return <code>true</code> is the javadoc is uptodate, <code>false</code> otherwise
     * @throws MavenReportException  if any error occur
     */
    private boolean isUpToDate( Commandline cmd )
            throws MavenReportException
    {
        try
        {
            String curdata = StaleHelper.getStaleData( cmd );
            Path cacheData = staleDataPath.toPath();
            String prvdata;
            if ( Files.isRegularFile( cacheData ) )
            {
                prvdata = new String( Files.readAllBytes( cacheData ), StandardCharsets.UTF_8 );
            }
            else
            {
                prvdata = null;
            }
            if ( curdata.equals( prvdata ) )
            {
                getLog().info( "Skipping javadoc generation, everything is up to date." );
                return true;
            }
            else
            {
                if ( prvdata == null )
                {
                    getLog().info( "No previous run data found, generating javadoc." );
                }
                else
                {
                    getLog().info( "Configuration changed, re-generating javadoc." );
                }
            }
        }
        catch ( IOException e )
        {
            throw new MavenReportException( "Error checking uptodate status", e );
        }
        return false;
    }

    /**
     * Execute the Javadoc command line
     *
     * @param cmd                    not null
     * @param javadocOutputDirectory not null
     * @throws MavenReportException if any errors occur
     */
    private void doExecuteJavadocCommandLine( Commandline cmd, File javadocOutputDirectory )
        throws MavenReportException
    {
        if ( getLog().isDebugEnabled() )
        {
            // no quoted arguments
            getLog().debug( CommandLineUtils.toString( cmd.getCommandline() ).replaceAll( "'", "" ) );
        }

        String cmdLine = null;
        if ( debug )
        {
            cmdLine = CommandLineUtils.toString( cmd.getCommandline() ).replaceAll( "'", "" );

            writeDebugJavadocScript( cmdLine, javadocOutputDirectory );
        }

        CommandLineUtils.StringStreamConsumer err = new JavadocUtil.JavadocOutputStreamConsumer();
        CommandLineUtils.StringStreamConsumer out = new JavadocUtil.JavadocOutputStreamConsumer();
        try
        {
            int exitCode = CommandLineUtils.executeCommandLine( cmd, out, err );

            String output = ( StringUtils.isEmpty( out.getOutput() ) ? null : '\n' + out.getOutput().trim() );

            if ( exitCode != 0 )
            {
                if ( cmdLine == null )
                {
                    cmdLine = CommandLineUtils.toString( cmd.getCommandline() ).replaceAll( "'", "" );
                }
                writeDebugJavadocScript( cmdLine, javadocOutputDirectory );

                if ( StringUtils.isNotEmpty( output ) && StringUtils.isEmpty( err.getOutput() )
                    && isJavadocVMInitError( output ) )
                {
                    throw new MavenReportException( output + '\n' + '\n' + JavadocUtil.ERROR_INIT_VM + '\n'
                        + "Or, try to reduce the Java heap size for the Javadoc goal using "
                        + "-Dminmemory=<size> and -Dmaxmemory=<size>." + '\n' + '\n' + "Command line was: " + cmdLine
                        + '\n' + '\n' + "Refer to the generated Javadoc files in '" + javadocOutputDirectory
                        + "' dir.\n" );
                }

                if ( StringUtils.isNotEmpty( output ) )
                {
                    getLog().info( output );
                }

                StringBuilder msg = new StringBuilder( "\nExit code: " );
                msg.append( exitCode );
                if ( StringUtils.isNotEmpty( err.getOutput() ) )
                {
                    msg.append( " - " ).append( err.getOutput() );
                }
                msg.append( '\n' );
                msg.append( "Command line was: " ).append( cmdLine ).append( '\n' ).append( '\n' );

                msg.append( "Refer to the generated Javadoc files in '" ).append( javadocOutputDirectory )
                .append( "' dir.\n" );

                throw new MavenReportException( msg.toString() );
            }

            if ( StringUtils.isNotEmpty( output ) )
            {
                getLog().info( output );
            }
        }
        catch ( CommandLineException e )
        {
            throw new MavenReportException( "Unable to execute javadoc command: " + e.getMessage(), e );
        }

        // ----------------------------------------------------------------------
        // Handle Javadoc warnings
        // ----------------------------------------------------------------------

        if ( containsWarnings( err.getOutput() ) )
        {
            if ( getLog().isWarnEnabled() )
            {
                getLog().warn( "Javadoc Warnings" );

                StringTokenizer token = new StringTokenizer( err.getOutput(), "\n" );
                while ( token.hasMoreTokens() )
                {
                    String current = token.nextToken().trim();

                    getLog().warn( current );
                }
            }

            if ( failOnWarnings )
            {
                throw new MavenReportException( "Project contains Javadoc Warnings" );
            }
        }
    }

    private boolean containsWarnings( String output )
    {
        // JDK-8268774 / JDK-8270831
        if ( this.javadocRuntimeVersion.isBefore( "17" ) )
        {
            return StringUtils.isNotEmpty( output );
        }
        else
        {
            return Arrays.stream( output.split( "\\R" ) )
                            .reduce( ( first, second ) -> second ) // last line
                            .filter( line -> line.matches( "\\d+ warnings?" ) )
                            .isPresent();
        }
    }

    /**
     * Patches the given Javadoc output directory to work around CVE-2013-1571
     * (see http://www.kb.cert.org/vuls/id/225657).
     *
     * @param javadocOutputDirectory directory to scan for vulnerabilities
     * @param outputEncoding         encoding used by the javadoc tool (-docencoding parameter).
     *                               If {@code null}, the platform's default encoding is used (like javadoc does).
     * @return the number of patched files
     */
    private int fixFrameInjectionBug( File javadocOutputDirectory, String outputEncoding )
        throws IOException
    {
        final String fixData;

        try ( InputStream in = this.getClass().getResourceAsStream( "frame-injection-fix.txt" ) )
        {
            if ( in == null )
            {
                throw new FileNotFoundException( "Missing resource 'frame-injection-fix.txt' in classpath." );
            }
            fixData = org.codehaus.plexus.util.StringUtils
                .unifyLineSeparators( IOUtil.toString( in, "US-ASCII" ) ).trim();
        }

        final DirectoryScanner ds = new DirectoryScanner();
        ds.setBasedir( javadocOutputDirectory );
        ds.setCaseSensitive( false );
        ds.setIncludes( new String[]{ "**/index.html", "**/index.htm", "**/toc.html", "**/toc.htm" } );
        ds.addDefaultExcludes();
        ds.scan();
        int patched = 0;
        for ( String f : ds.getIncludedFiles() )
        {
            final File file = new File( javadocOutputDirectory, f );
            // we load the whole file as one String (toc/index files are
            // generally small, because they only contain frameset declaration):
            final String fileContents = FileUtils.fileRead( file, outputEncoding );
            // check if file may be vulnerable because it was not patched with "validURL(url)":
            if ( !StringUtils.contains( fileContents, "function validURL(url) {" ) )
            {
                // we need to patch the file!
                final String patchedFileContents =
                    StringUtils.replaceOnce( fileContents, "function loadFrames() {", fixData );
                if ( !patchedFileContents.equals( fileContents ) )
                {
                    FileUtils.fileWrite( file, outputEncoding, patchedFileContents );
                    patched++;
                }
            }
        }
        return patched;
    }

    /**
     * @param outputFile        not nul
     * @param inputResourceName a not null resource in <code>src/main/java</code>, <code>src/main/resources</code> or
     *                          <code>src/main/javadoc</code> or in the Javadoc plugin dependencies.
     * @return the resource file absolute path as String
     * @since 2.6
     */
    private Optional<File> getResource( File outputFile, String inputResourceName )
    {
        if ( inputResourceName.startsWith( "/" ) )
        {
            inputResourceName = inputResourceName.replaceFirst( "//*", "" );
        }

        List<String> classPath = new ArrayList<>();
        classPath.add( project.getBuild().getSourceDirectory() );

        URL resourceURL = getResource( classPath, inputResourceName );
        if ( resourceURL != null )
        {
            getLog().debug( inputResourceName + " found in the main src directory of the project." );
            return Optional.of( FileUtils.toFile( resourceURL ) );
        }

        classPath.clear();
        List<Resource> resources = project.getBuild().getResources();
        for ( Resource resource : resources )
        {
            classPath.add( resource.getDirectory() );
        }
        resourceURL = getResource( classPath, inputResourceName );
        if ( resourceURL != null )
        {
            getLog().debug( inputResourceName + " found in the main resources directories of the project." );
            return Optional.of( FileUtils.toFile( resourceURL ) );
        }

        if ( javadocDirectory.exists() )
        {
            classPath.clear();
            classPath.add( javadocDirectory.getAbsolutePath() );
            resourceURL = getResource( classPath, inputResourceName );
            if ( resourceURL != null )
            {
                getLog().debug( inputResourceName + " found in the main javadoc directory of the project." );
                return Optional.of( FileUtils.toFile( resourceURL ) );
            }
        }

        classPath.clear();
        final String pluginId = "org.apache.maven.plugins:maven-javadoc-plugin";
        Plugin javadocPlugin = getPlugin( project, pluginId );
        if ( javadocPlugin != null && javadocPlugin.getDependencies() != null )
        {
            List<Dependency> dependencies = javadocPlugin.getDependencies();
            for ( Dependency dependency : dependencies )
            {
                ResourcesArtifact resourceArtifact = new ResourcesArtifact();
                resourceArtifact.setGroupId( dependency.getGroupId() );
                resourceArtifact.setArtifactId( dependency.getArtifactId() );
                resourceArtifact.setVersion( dependency.getVersion() );
                resourceArtifact.setClassifier( dependency.getClassifier() );
                Artifact artifact = null;
                try
                {
                    artifact = createAndResolveArtifact( resourceArtifact );
                }
                catch ( Exception e )
                {
                    logError( "Unable to retrieve the dependency: " + dependency + ". Ignored.", e );
                }

                if ( artifact != null && artifact.getFile().exists() )
                {
                    classPath.add( artifact.getFile().getAbsolutePath() );
                }
            }
            resourceURL = getResource( classPath, inputResourceName );
            if ( resourceURL != null )
            {
                getLog().debug( inputResourceName + " found in javadoc plugin dependencies." );
                try
                {
                    JavadocUtil.copyResource( resourceURL, outputFile );

                    return Optional.of( outputFile );
                }
                catch ( IOException e )
                {
                    logError( "IOException: " + e.getMessage(), e );
                }
            }
        }

        getLog().warn( "Unable to find the resource '" + inputResourceName + "'. Using default Javadoc resources." );

        return Optional.empty();
    }

    /**
     * @param classPath a not null String list of files where resource will be looked up
     * @param resource a not null resource to find in the class path
     * @return the resource from the given classpath or null if not found
     * @see ClassLoader#getResource(String)
     * @since 2.6
     */
    private URL getResource( final List<String> classPath, final String resource )
    {
        List<URL> urls = new ArrayList<>( classPath.size() );
        for ( String filename : classPath )
        {
            try
            {
                urls.add( new File( filename ).toURI().toURL() );
            }
            catch ( MalformedURLException e )
            {
                getLog().error( "MalformedURLException: " + e.getMessage() );
            }
        }

        URLClassLoader javadocClassLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), null );
        try
        {
            return javadocClassLoader.getResource( resource );
        }
        finally
        {
            try
            {
                javadocClassLoader.close();
            }
            catch ( IOException ex )
            {
                // ignore
            }
        }
    }

    /**
     * Get the full javadoc goal. Loads the plugin's pom.properties to get the current plugin version.
     *
     * @return <code>org.apache.maven.plugins:maven-javadoc-plugin:CURRENT_VERSION:[test-]javadoc</code>
     */
    private String getFullJavadocGoal()
    {
        String javadocPluginVersion = null;
        String resource = "META-INF/maven/org.apache.maven.plugins/maven-javadoc-plugin/pom.properties";
        try ( InputStream resourceAsStream
                        = AbstractJavadocMojo.class.getClassLoader().getResourceAsStream( resource ) )
        {
            if ( resourceAsStream != null )
            {
                Properties properties = new Properties();
                properties.load( resourceAsStream );
                if ( StringUtils.isNotEmpty( properties.getProperty( "version" ) ) )
                {
                    javadocPluginVersion = properties.getProperty( "version" );
                }
            }
        }
        catch ( IOException e )
        {
            // nop
        }

        StringBuilder sb = new StringBuilder();

        sb.append( "org.apache.maven.plugins:maven-javadoc-plugin:" );
        if ( StringUtils.isNotEmpty( javadocPluginVersion ) )
        {
            sb.append( javadocPluginVersion ).append( ":" );
        }

        if ( this instanceof TestJavadocReport )
        {
            sb.append( "test-javadoc" );
        }
        else
        {
            sb.append( "javadoc" );
        }

        return sb.toString();
    }

    /**
     * Using Maven, a Javadoc link is given by <code>${project.url}/apidocs</code>.
     *
     * @return the detected Javadoc links using the Maven conventions for all modules defined in the current project
     *         or an empty list
     * @throws MavenReportException if any
     * @see #detectOfflineLinks
     * @see #reactorProjects
     * @since 2.6
     */
    private List<OfflineLink> getModulesLinks()
        throws MavenReportException
    {
        List<MavenProject> aggregatedProjects = reactorProjects;
        if ( !detectOfflineLinks || isAggregator() || aggregatedProjects.isEmpty() )
        {
            return Collections.emptyList();
        }

        getLog().debug( "Trying to add links for modules..." );

        Set<String> dependencyArtifactIds = new HashSet<>();
        final Set<Artifact> dependencyArtifacts = project.getDependencyArtifacts();
        for ( Artifact artifact : dependencyArtifacts )
        {
            dependencyArtifactIds.add( artifact.getId() );
        }

        List<OfflineLink> modulesLinks = new ArrayList<>();
        String javadocDirRelative = PathUtils.toRelative( project.getBasedir(), getOutputDirectory() );
        for ( MavenProject p : aggregatedProjects )
        {
            if ( !dependencyArtifactIds.contains( p.getArtifact().getId() ) || ( p.getUrl() == null ) )
            {
                continue;
            }

            File location = new File( p.getBasedir(), javadocDirRelative );

            if ( !location.exists() )
            {
                if ( getLog().isDebugEnabled() )
                {
                    getLog().debug( "Javadoc directory not found: " + location );
                }

                String javadocGoal = getFullJavadocGoal();
                getLog().info(
                    "The goal '" + javadocGoal + "' has not been previously called for the module: '" + p.getId()
                        + "'. Trying to invoke it..." );

                File invokerDir = new File( project.getBuild().getDirectory(), "invoker" );
                invokerDir.mkdirs();
                File invokerLogFile = FileUtils.createTempFile( "maven-javadoc-plugin", ".txt", invokerDir );
                try
                {
                    JavadocUtil.invokeMaven( getLog(), new File( localRepository.getBasedir() ), p.getFile(),
                                             Collections.singletonList( javadocGoal ), null, invokerLogFile,
                                             session.getRequest().getGlobalSettingsFile() );
                }
                catch ( MavenInvocationException e )
                {
                    logError( "MavenInvocationException: " + e.getMessage(), e );

                    String invokerLogContent = JavadocUtil.readFile( invokerLogFile, null /* platform encoding */ );

                    // TODO: Why are we only interested in cases where the JVM won't start?
                    // [MJAVADOC-275][jdcasey] I changed the logic here to only throw an error WHEN
                    //   the JVM won't start (opposite of what it was).
                    if ( invokerLogContent != null && invokerLogContent.contains( JavadocUtil.ERROR_INIT_VM ) )
                    {
                        throw new MavenReportException( e.getMessage(), e );
                    }
                }
                finally
                {
                    // just create the directory to prevent repeated invocations..
                    if ( !location.exists() )
                    {
                        getLog().warn( "Creating fake javadoc directory to prevent repeated invocations: " + location );
                        location.mkdirs();
                    }
                }
            }

            if ( location.exists() )
            {
                String url = getJavadocLink( p );

                OfflineLink ol = new OfflineLink();
                ol.setUrl( url );
                ol.setLocation( location.getAbsolutePath() );

                if ( getLog().isDebugEnabled() )
                {
                    getLog().debug( "Added Javadoc offline link: " + url + " for the module: " + p.getId() );
                }

                modulesLinks.add( ol );
            }
        }

        return modulesLinks;
    }

    /**
     * Using Maven, a Javadoc link is given by <code>${project.url}/apidocs</code>.
     *
     * @return the detected Javadoc links using the Maven conventions for all dependencies defined in the current
     *         project or an empty list.
     * @see #detectLinks
     * @see #isValidJavadocLink(String, boolean)
     * @since 2.6
     */
    private List<String> getDependenciesLinks()
    {
        if ( !detectLinks )
        {
            return Collections.emptyList();
        }

        getLog().debug( "Trying to add links for dependencies..." );

        List<String> dependenciesLinks = new ArrayList<>();

        final Set<Artifact> dependencies = project.getDependencyArtifacts();
        for ( Artifact artifact : dependencies )
        {
            if ( artifact.getFile() == null || !artifact.getFile().exists() )
            {
                continue;
            }

            Optional<DependencyLink> depLink =
                this.dependencyLinks.stream().filter( d -> matches( d, artifact ) ).findAny();

            final String url;
            final boolean detected;
            if ( depLink.isPresent() )
            {
                url = depLink.get().getUrl();
                detected = false;
            }
            else
            {
                try
                {
                    MavenProject artifactProject =
                        mavenProjectBuilder.build( artifact, getProjectBuildingRequest( project ) ).getProject();

                    url = getJavadocLink( artifactProject );
                    detected = true;
                }
                catch ( ProjectBuildingException e )
                {
                    logError( "ProjectBuildingException for " + artifact.toString() + ": " + e.getMessage(), e );
                    continue;
                }
            }

            if ( url != null && isValidJavadocLink( url, detected ) )
            {
                getLog().debug( "Added Javadoc link: " + url + " for " + artifact.getId() );

                dependenciesLinks.add( url );
            }
        }

        return dependenciesLinks;
    }

    private boolean matches( DependencyLink d, Artifact artifact )
    {
        if ( d.getGroupId() != null && !d.getGroupId().equals( artifact.getGroupId() ) )
        {
            return false;
        }
        if ( d.getArtifactId() != null && !d.getArtifactId().equals( artifact.getArtifactId() ) )
        {
            return false;
        }
        if ( d.getClassifier() != null && !d.getClassifier().equals( artifact.getClassifier() ) )
        {
            return false;
        }
        return true;
    }

    /**
     * @return if {@code detectJavaApiLink}, the Java API link based on the {@code javaApiLinks} properties and the
     *         value of the <code>source</code> parameter in the
     *         <code>org.apache.maven.plugins:maven-compiler-plugin</code>
     *         defined in <code>${project.build.plugins}</code> or in <code>${project.build.pluginManagement}</code>,
     *         or the {@code javadocRuntimeVersion}, or <code>null</code> if not defined.
     * @see <a href="http://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#source">source parameter</a>
     * @since 2.6
     */
    protected final OfflineLink getDefaultJavadocApiLink()
    {
        if ( !detectJavaApiLink )
        {
            return null;
        }

        final JavaVersion javaApiversion;
        if ( release != null )
        {
            javaApiversion = JavaVersion.parse( release );
        }
        else if ( source != null && !source.isEmpty() )
        {
            javaApiversion = JavaVersion.parse( source );
        }
        else
        {
            final String pluginId = "org.apache.maven.plugins:maven-compiler-plugin";
            String sourceConfigured = getPluginParameter( project, pluginId, "source" );
            if ( sourceConfigured != null )
            {
                javaApiversion = JavaVersion.parse( sourceConfigured );
            }
            else
            {
                getLog().debug( "No maven-compiler-plugin defined in ${build.plugins} or in "
                                    + "${project.build.pluginManagement} for the " + project.getId()
                                    + ". Added Javadoc API link according the javadoc executable version i.e.: "
                                    + javadocRuntimeVersion );

                javaApiversion = javadocRuntimeVersion;
            }
        }

        final String javaApiKey;
        if ( javaApiversion.asMajor().isAtLeast( "9" ) )
        {
            javaApiKey = "api_" + javaApiversion.asMajor();
        }
        else
        {
            javaApiKey = "api_1." + javaApiversion.asMajor().toString().charAt( 0 );
        }

        final String javaApiLink;
        if ( javaApiLinks != null && javaApiLinks.containsKey( javaApiKey ) )
        {
            javaApiLink = javaApiLinks.getProperty( javaApiKey );
        }
        else if ( javaApiversion.isAtLeast( "16" ) )
        {
            javaApiLink = null; // JDK-8216497
        }
        else if ( javaApiversion.isAtLeast( "11" ) )
        {
            javaApiLink =
                String.format( "https://docs.oracle.com/en/java/javase/%s/docs/api/", javaApiversion.getValue( 1 ) );
        }
        else if ( javaApiversion.asMajor().isAtLeast( "6" ) )
        {
            javaApiLink =
                String.format( "https://docs.oracle.com/javase/%s/docs/api/", javaApiversion.asMajor().getValue( 1 ) );
        }
        else if ( javaApiversion.isAtLeast( "1.5" ) )
        {
            javaApiLink = "https://docs.oracle.com/javase/1.5.0/docs/api/";
        }
        else
        {
            javaApiLink = null;
        }

        if ( getLog().isDebugEnabled() )
        {
            if ( javaApiLink != null )
            {
                getLog().debug( "Found Java API link: " + javaApiLink );
            }
            else
            {
                getLog().debug( "No Java API link found." );
            }
        }

        if ( javaApiLink == null )
        {
            return null;
        }

        final Path javaApiListFile;
        final String resourceName;
        if ( javaApiversion.isAtLeast( "10" ) )
        {
            javaApiListFile = getJavadocOptionsFile().getParentFile().toPath().resolve( "element-list" );
            resourceName = "java-api-element-list-" + javaApiversion.toString().substring( 0, 2 );
        }
        else if ( javaApiversion.asMajor().isAtLeast( "9" ) )
        {
            javaApiListFile = getJavadocOptionsFile().getParentFile().toPath().resolve( "package-list" );
            resourceName = "java-api-package-list-9";
        }
        else
        {
            javaApiListFile = getJavadocOptionsFile().getParentFile().toPath().resolve( "package-list" );
            resourceName = "java-api-package-list-1." + javaApiversion.asMajor().toString().charAt( 0 );
        }

        OfflineLink link = new OfflineLink();
        link.setLocation( javaApiListFile.getParent().toAbsolutePath().toString() );
        link.setUrl( javaApiLink );

        InputStream in = this.getClass().getResourceAsStream( resourceName );
        if ( in != null )
        {
            try ( InputStream closableIS = in )
            {
                // TODO only copy when changed
                Files.copy( closableIS, javaApiListFile, StandardCopyOption.REPLACE_EXISTING );
            }
            catch ( IOException ioe )
            {
                logError( "Can't get " + resourceName + ": " + ioe.getMessage(), ioe );
                return null;
            }
        }

        return link;
    }

    /**
     * Follows all of the given links, and returns their last redirect locations. Ordering is kept.
     * This is necessary because javadoc tool doesn't follow links, see JDK-8190312 (MJAVADOC-427, MJAVADOC-487)
     *
     * @param links Links to follow.
     * @return Last redirect location of all the links.
     */
    private Set<String> followLinks( Set<String> links )
    {
        Set<String> redirectLinks = new LinkedHashSet<>( links.size() );
        for ( String link : links )
        {
            try
            {
                redirectLinks.add( JavadocUtil.getRedirectUrl( new URI( link ).toURL(), settings ).toString() );
            }
            catch ( Exception e )
            {
                // only print in debug, it should have been logged already in warn/error because link isn't valid
                getLog().debug( "Could not follow " + link + ". Reason: " + e.getMessage() );

                // Even when link produces error it should be kept in the set because the error might be caused by
                // incomplete redirect configuration on the server side.
                // This partially restores the previous behaviour before fix for MJAVADOC-427
                redirectLinks.add( link );
            }
        }
        return redirectLinks;
    }

    /**
     * @param link not null
     * @param detecting <code>true</code> if the link is generated by
     * <code>detectLinks</code>, or <code>false</code> otherwise
     * @return <code>true</code> if the link has a <code>/package-list</code>, <code>false</code> otherwise.
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/solaris/javadoc.html#package-list">
     *      package-list spec</a>
     * @since 2.6
     */
    protected boolean isValidJavadocLink( String link, boolean detecting )
    {
        try
        {
            final URI packageListUri;
            final URI elementListUri;

            if ( link.trim().toLowerCase( Locale.ENGLISH ).startsWith( "http:" ) || link.trim().toLowerCase(
                Locale.ENGLISH ).startsWith( "https:" ) || link.trim().toLowerCase( Locale.ENGLISH ).startsWith(
                "ftp:" ) || link.trim().toLowerCase( Locale.ENGLISH ).startsWith( "file:" ) )
            {
                packageListUri = new URI( link + '/' + PACKAGE_LIST );
                elementListUri = new URI( link + '/' + ELEMENT_LIST );
            }
            else
            {
                // links can be relative paths or files
                File dir = new File( link );
                if ( !dir.isAbsolute() )
                {
                    dir = new File( getOutputDirectory(), link );
                }
                if ( !dir.isDirectory() )
                {
                    if ( detecting )
                    {
                        getLog().warn( "The given File link: " + dir + " is not a dir." );
                    }
                    else
                    {
                        getLog().error( "The given File link: " + dir + " is not a dir." );
                    }
                }
                packageListUri = new File( dir, PACKAGE_LIST ).toURI();
                elementListUri = new File( dir, ELEMENT_LIST ).toURI();
            }


            try
            {
                if ( JavadocUtil.isValidElementList( elementListUri.toURL(), settings, validateLinks ) )
                {
                    return true;
                }
            }
            catch ( IOException e )
            {
            }

            if ( JavadocUtil.isValidPackageList( packageListUri.toURL(), settings, validateLinks ) )
            {
                return true;
            }

            if ( getLog().isErrorEnabled() )
            {
                if ( detecting )
                {
                    getLog().warn( "Invalid links: "
                                    + link + " with /" + PACKAGE_LIST + " or / " + ELEMENT_LIST + ". Ignored it." );
                }
                else
                {
                    getLog().error( "Invalid links: "
                                    + link + " with /" + PACKAGE_LIST + " or / " + ELEMENT_LIST + ". Ignored it." );
                }
            }

            return false;
        }
        catch ( URISyntaxException e )
        {
            if ( getLog().isErrorEnabled() )
            {
                if ( detecting )
                {
                    getLog().warn( "Malformed link: " + e.getInput() + ". Ignored it." );
                }
                else
                {
                    getLog().error( "Malformed link: " + e.getInput() + ". Ignored it." );
                }
            }
            return false;
        }
        catch ( IOException e )
        {
            if ( getLog().isErrorEnabled() )
            {
                if ( detecting )
                {
                    getLog().warn( "Error fetching link: " + link + ". Ignored it." );
                }
                else
                {
                    getLog().error( "Error fetching link: " + link + ". Ignored it." );
                }
            }
            return false;
        }
    }

    /**
     * Write a debug javadoc script in case of command line error or in debug mode.
     *
     * @param cmdLine                the current command line as string, not null.
     * @param javadocOutputDirectory the output dir, not null.
     * @see #executeJavadocCommandLine(Commandline, File)
     * @since 2.6
     */
    private void writeDebugJavadocScript( String cmdLine, File javadocOutputDirectory )
    {
        File commandLineFile = new File( javadocOutputDirectory, DEBUG_JAVADOC_SCRIPT_NAME );
        commandLineFile.getParentFile().mkdirs();

        try
        {
            FileUtils.fileWrite( commandLineFile.getAbsolutePath(), null /* platform encoding */, cmdLine );

            if ( !SystemUtils.IS_OS_WINDOWS )
            {
                Runtime.getRuntime().exec( new String[]{ "chmod", "a+x", commandLineFile.getAbsolutePath() } );
            }
        }
        catch ( IOException e )
        {
            logError( "Unable to write '" + commandLineFile.getName() + "' debug script file", e );
        }
    }

    /**
     * Check if the Javadoc JVM is correctly started or not.
     *
     * @param output the command line output, not null.
     * @return <code>true</code> if Javadoc output command line contains Javadoc word, <code>false</code> otherwise.
     * @see #executeJavadocCommandLine(Commandline, File)
     * @since 2.6.1
     */
    private boolean isJavadocVMInitError( String output )
    {
        /*
         * see main.usage and main.Building_tree keys from
         * com.sun.tools.javadoc.resources.javadoc bundle in tools.jar
         */
        return !( output.contains( "Javadoc" ) || output.contains( "javadoc" ) );
    }

    // ----------------------------------------------------------------------
    // Static methods
    // ----------------------------------------------------------------------

    /**
     * @param p not null
     * @return the javadoc link based on the project url i.e. <code>${project.url}/${destDir}</code> where
     *         <code>destDir</code> is configued in the Javadoc plugin configuration (<code>apidocs</code> by default).
     * @since 2.6
     */
    private static String getJavadocLink( MavenProject p )
    {
        if ( p.getUrl() == null )
        {
            return null;
        }

        String url = cleanUrl( p.getUrl() );
        String destDir = "apidocs"; // see JavadocReport#destDir

        final String pluginId = "org.apache.maven.plugins:maven-javadoc-plugin";
        String destDirConfigured = getPluginParameter( p, pluginId, "destDir" );
        if ( destDirConfigured != null )
        {
            destDir = destDirConfigured;
        }

        return url + "/" + destDir;
    }

    /**
     * @param url could be null.
     * @return the url cleaned or empty if url was null.
     * @since 2.6
     */
    private static String cleanUrl( String url )
    {
        if ( url == null )
        {
            return "";
        }

        url = url.trim();
        while ( url.endsWith( "/" ) )
        {
            url = url.substring( 0, url.lastIndexOf( "/" ) );
        }

        return url;
    }

    /**
     * @param p        not null
     * @param pluginId not null key of the plugin defined in {@link org.apache.maven.model.Build#getPluginsAsMap()}
     *                 or in {@link org.apache.maven.model.PluginManagement#getPluginsAsMap()}
     * @return the Maven plugin defined in <code>${project.build.plugins}</code> or in
     *         <code>${project.build.pluginManagement}</code>, or <code>null</code> if not defined.
     * @since 2.6
     */
    private static Plugin getPlugin( MavenProject p, String pluginId )
    {
        if ( ( p.getBuild() == null ) || ( p.getBuild().getPluginsAsMap() == null ) )
        {
            return null;
        }

        Plugin plugin = p.getBuild().getPluginsAsMap().get( pluginId );

        if ( ( plugin == null ) && ( p.getBuild().getPluginManagement() != null ) && (
            p.getBuild().getPluginManagement().getPluginsAsMap() != null ) )
        {
            plugin = p.getBuild().getPluginManagement().getPluginsAsMap().get( pluginId );
        }

        return plugin;
    }

    /**
     * @param p        not null
     * @param pluginId not null
     * @param param    not null
     * @return the simple parameter as String defined in the plugin configuration by <code>param</code> key
     *         or <code>null</code> if not found.
     * @since 2.6
     */
    private static String getPluginParameter( MavenProject p, String pluginId, String param )
    {
//        p.getGoalConfiguration( pluginGroupId, pluginArtifactId, executionId, goalId );
        Plugin plugin = getPlugin( p, pluginId );
        if ( plugin != null )
        {
            Xpp3Dom xpp3Dom = (Xpp3Dom) plugin.getConfiguration();
            if ( xpp3Dom != null && xpp3Dom.getChild( param ) != null
                && StringUtils.isNotEmpty( xpp3Dom.getChild( param ).getValue() ) )
            {
                return xpp3Dom.getChild( param ).getValue();
            }
        }

        return null;
    }

    /**
     * Construct the output file for the generated javadoc-options XML file, after creating the
     * javadocOptionsDir if necessary. This method does NOT write to the file in question.
     *
     * @return The options {@link File} file.
     * @since 2.7
     */
    protected final File getJavadocOptionsFile()
    {
        if ( javadocOptionsDir != null && !javadocOptionsDir.exists() )
        {
            javadocOptionsDir.mkdirs();
        }

        return new File( javadocOptionsDir, "javadoc-options-" + getAttachmentClassifier() + ".xml" );
    }

    /**
     * Generate a javadoc-options XML file, for either bundling with a javadoc-resources artifact OR
     * supplying to a distro module in a includeDependencySources configuration, so the javadoc options
     * from this execution can be reconstructed and merged in the distro build.
     *
     * @return {@link JavadocOptions}
     * @throws IOException {@link IOException}
     * @since 2.7
     */
    protected final JavadocOptions buildJavadocOptions()
        throws IOException
    {
        JavadocOptions options = new JavadocOptions();

        options.setBootclasspathArtifacts( toList( bootclasspathArtifacts ) );
        options.setDocfilesSubdirsUsed( docfilessubdirs );
        options.setDocletArtifacts( toList( docletArtifact, docletArtifacts ) );
        options.setExcludedDocfilesSubdirs( excludedocfilessubdir );
        options.setExcludePackageNames( toList( excludePackageNames ) );
        options.setGroups( toList( groups ) );
        options.setLinks( links );
        options.setOfflineLinks( toList( offlineLinks ) );
        options.setResourcesArtifacts( toList( resourcesArtifacts ) );
        options.setTagletArtifacts( toList( tagletArtifact, tagletArtifacts ) );
        options.setTaglets( toList( taglets ) );
        options.setTags( toList( tags ) );

        if ( getProject() != null && getJavadocDirectory() != null )
        {
            options.setJavadocResourcesDirectory(
                toRelative( getProject().getBasedir(), getJavadocDirectory().getAbsolutePath() ) );
        }

        File optionsFile = getJavadocOptionsFile();

        try ( Writer writer = WriterFactory.newXmlWriter( optionsFile ) )
        {
            new JavadocOptionsXpp3Writer().write( writer, options );
        }

        return options;
    }

    /**
     * Override this if you need to provide a bundle attachment classifier, as in the case of test
     * javadocs.
     * @return The attachment classifier.
     */
    protected String getAttachmentClassifier()
    {
        return JAVADOC_RESOURCES_ATTACHMENT_CLASSIFIER;
    }

    /**
     * Logs an error with throwable content only if in debug.
     *
     * @param message The message which should be announced.
     * @param t The throwable part of the message.
     */
    protected void logError( String message, Throwable t )
    {
        if ( getLog().isDebugEnabled() )
        {
            getLog().error( message, t );
        }
        else
        {
            getLog().error( message );
        }
    }

    /**
     * @param prefix The prefix of the exception.
     * @param e The exception.
     * @throws MojoExecutionException {@link MojoExecutionException} issue while generating report
     */
    protected void failOnError( String prefix, Exception e )
        throws MojoExecutionException
    {
        if ( failOnError )
        {
            if ( e instanceof RuntimeException )
            {
                throw (RuntimeException) e;
            }
            throw new MojoExecutionException( prefix + ": " + e.getMessage(), e );
        }

        getLog().error( prefix + ": " + e.getMessage(), e );
    }


    /**
     *
     * @return List of projects to be part of aggregated javadoc
     */
    private List<MavenProject> getAggregatedProjects()
    {
        if ( this.reactorProjects == null )
        {
           return Collections.emptyList();
        }
        Map<Path, MavenProject> reactorProjectsMap = new HashMap<>();
        for ( MavenProject reactorProject : this.reactorProjects )
        {
            if ( !isSkippedJavadoc( reactorProject ) && //
                    !isSkippedModule( reactorProject ) )
            {
                reactorProjectsMap.put( reactorProject.getBasedir().toPath(), reactorProject );
            }
        }

        return new ArrayList<>( modulesForAggregatedProject( project, reactorProjectsMap ) );
    }

    /**
     *
     * @return <code>true</code> if the module need to be skipped from aggregate generation
     */
    protected boolean isSkippedModule( MavenProject mavenProject )
    {
        if ( StringUtils.isEmpty( this.skippedModules ) )
        {
            return false;
        }
        List<String> modulesToSkip = Arrays.asList( StringUtils.split( this.skippedModules, ',' ) );
        return modulesToSkip.contains( mavenProject.getArtifactId() );
    }

    /**
     *
     * @return <code>true</code> if the pom configuration skip javadoc generation for the project
     */
    protected boolean isSkippedJavadoc( MavenProject mavenProject )
    {
        String property = mavenProject.getProperties().getProperty( "maven.javadoc.skip" );
        if ( property != null )
        {
            boolean skip = BooleanUtils.toBoolean( property );
            getLog().debug( "isSkippedJavadoc " + mavenProject + " " + skip );
            return skip;
        }
        final String pluginId = "org.apache.maven.plugins:maven-javadoc-plugin";
        property = getPluginParameter( mavenProject, pluginId, "skip" );
        if ( property != null )
        {
            boolean skip = BooleanUtils.toBoolean( property );
            getLog().debug( "isSkippedJavadoc " + mavenProject + " " + skip );
            return skip;
        }
        if ( mavenProject.getParent() != null )
        {
            return isSkippedJavadoc( mavenProject.getParent() );
        }
        getLog().debug( "isSkippedJavadoc " + mavenProject + " " + false );
        return false;
    }

}
