JJDocGoalMojo.java

/*
 * Copyright (c) 2025-2026, Marc Mazas <mazas.marc@gmail.com>.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the names of the copyright holders nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.javacc.mojo;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;

import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

/**
 * The <b>jjdoc</b> goal, for producing documentation for a JavaCC / JJTree / JTB grammar.<br>
 * It is used indirectly under the hood when using the plugin as a reporting plugin, triggered by
 * the Maven site plugin, producing JJDoc reports. In that case there can be only one execution and
 * one configuration defining the source directories and other parameters.<br>
 * It can also be used directly as a standalone goal, as a build plugin, if one wants more control,
 * for example for multiple executions with different configurations with different sets of source
 * directories and other parameters.<br>
 * It searches the source directories for all grammar files (those included and not excluded) and
 * runs JJDoc once for each file it finds, producing output files of the format set through the
 * JJDoc options, and of names set through JJDoc options <code>OUTPUT_DIRECTORY</code> and
 * <code>OUTPUT_FILE</code>.<br>
 * It also produces, for each execution, an HTML "index" file named through the plugin parameter
 * <code>jjdocReportsPage</code> containing a table with the hyperlinks for the grammar file name to
 * its corresponding generated JJDoc document, in a directory set by a plugin parameter
 * <code>jjdocReportsDirectory</code> if used in a build plugin or by the Maven site plugin
 * parameter <code>outputDirectory</code> if used in a reporting plugin.<br>
 * And finally, if used in a reporting plugin, the Maven site plugin will create a menu entry to the
 * HTML "index" page in the "Project Documentation / Project Reports" menu of the site.
 * <p>
 * Detailed information about the JJDoc options can be found on the
 * <a href="https://javacc.github.io/javacc-8/documentation/jjdoc.html">JJDoc documentation
 * page</a>.<br>
 * Examples can be found in the integration tests <a href=
 * "https://github.com/javacc/javacc-maven-plugin/tree/master/src/it/jjdoc-goal">jjdoc-goal</a> and
 * <a href=
 * "https://github.com/javacc/javacc-maven-plugin/tree/master/src/it/site-phase">site-phase</a>.<br>
 * The code repositories can be found within <a href="https://github.com/javacc">JavaCC at
 * GitHub</a> and <a href="https://github.com/jtb-javacc">JTB at GitHub</a>.
 *
 * @since 3.8.0
 * @author Maͫzͣaͬsͨ
 */
@Mojo(name = "jjdoc", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true)
public class JJDocGoalMojo extends AbstractPluginReport {
  
  /** The bean handling JJDoc options as command line arguments. */
  private final JJDocArgumentsBean jjdab = new JJDocArgumentsBean();
  
  /**
   * The list of single full command line arguments, which should be in the form accepted by JJDoc.
   *
   * <p>
   * No arguments are read and used by the plugin.
   *
   * <p>
   * The list will be passed as it is to JJDoc, no control nor modification is done by the
   * plugin.<br>
   * Note that JJDoc can internally pass some options to the parser it calls.
   *
   * <p>
   * The list has no default value.
   *
   * <p>
   * Example:<br>
   *
   * <pre>{@code
   * <jjdocCmdLineArgs>
   *   <arg>-CODE_GENERATOR="C#"</arg>
   *   <arg>-BNF=true</arg>
   *   <arg>-CSS="src/main/resources/my_css"</arg>
   * </jjdocCmdLineArgs>
   * }</pre>
   *
   * <br>
   * Note that the <code>javaccCmdLineArgs</code> parameter is of type {@code List<String>}, which
   * implies that the inner tags names can have any names and may be appear many times (like
   * <code>arg</code> above).<br>
   * Note also that if the project has a parent which also configures the plugin with parameters (to
   * factorize them for the children for example), the child parameters will complement / replace
   * the parent's ones for those that are absent / present in the parent; so
   * <ul>
   * <li>if one wants to get rid of (all) the parent's ones, he must use the <code>
   * combine.self="override"</code> attribute at the list level in the child, and</li>
   * <li>if one wants to replace some of the parent's ones and add new ones in the child it is
   * recommended to use distinct tag names in the parent, the same tag names in the child for those
   * that must be replaced and another tag name or other tag names for the new ones.</li>
   * </ul>
   * See also <a href=
   * "https://github.com/javacc/javacc-maven-plugin#processor-parameters">Processor
   * parameters</a>.
   */
  @Parameter(property = "javacc.jjdocCmdLineArgs") //
  protected List<String> jjdocCmdLineArgs;
  
  /**
   * The output directory where Maven / Doxia generates the HTML files summarizing the JJDoc reports
   * generation.<br>
   * Note that this parameter is only relevant if the goal is run from the command line or from the
   * default build lifecycle.<br>
   * If the goal is run indirectly as part of a site generation, the output directory configured in
   * the Maven Site Plugin is used instead (it will usually be "${project.build.directory}/site").
   * <p>
   * Note that this is not the directory where JJDoc itself generates its reports, which must be set
   * by the JJDoc OUTPUT_DIRECTORY option.
   *
   */
  @Parameter(property = "javacc.jjdocReportsDirectory", defaultValue = "${project.build.directory}/generated-jjdoc") //
  private File jjdocReportsDirectory;
  
  /**
   * The HTML page name (with extension), relative to the output directory, where Maven / Doxia
   * generates the table of the JJDoc reports for the current execution.<br>
   * Use a non default value for each of multiple executions, to differentiate reports.
   */
  @Parameter(property = "javacc.jjdocReportsPage", defaultValue = "/jjdoc-reports.html") //
  protected String jjdocReportsPage;
  
  @Override
  protected void initProcessor() throws PluginException {
    if (includes == null) {
      includes = new String[] {
          "**/*.jj", "**/*.jjt", "**/*.jtb"
      };
      getLog().debug("no custom includes, so initialized to '" + Arrays.toString(includes) + "'");
    } else {
      getLog().debug("custom includes is '" + Arrays.toString(includes) + "'");
    }
    boolean custom = true;
    if (sourceDirectories == null) {
      custom = false;
      sourceDirectories = new ArrayList<>();
    }
    if (sourceDirectories.isEmpty()) {
      final File jjdir = new File(project.getBasedir(), JavaCCArgumentsBean.defSrcSubDir);
      if (jjdir.exists()) {
        sourceDirectories.add(jjdir);
      }
      final File jjtdir = new File(project.getBasedir(), JJTreeArgumentsBean.defSrcSubDir);
      if (jjtdir.exists()) {
        sourceDirectories.add(jjtdir);
      }
      final File jtbdir = new File(project.getBasedir(), JTBArgumentsBean.defSrcSubDir);
      if (jtbdir.exists()) {
        sourceDirectories.add(jtbdir);
      }
    }
    if (custom) {
      getLog().debug("custom sourceDirectories is '" + displayDirectories(sourceDirectories) + "'");
    } else {
      getLog().debug("no custom sourceDirectories, so initialized to '"
          + displayDirectories(sourceDirectories) + "'");
    }
    getLog().debug("jjdocReportsPage is '" + jjdocReportsPage + "'");
    getLog().debug("jjdocReportsDirectory is '" + jjdocReportsDirectory + "'");
    
    // mojo's log injected after the mojo is constructed, so wait until now to pass it to the bean
    jjdab.log = getLog();
    jjdab.setProcCmdLineArgs(jjdocCmdLineArgs);
    jjdab.findSpecificOptions(project, "jjdocCmdLineArgs");
  }
  
  @Override
  protected void processGrammar(final GrammarInfo grammarInfo) throws ProcessorException {
    getLog().debug("processGrammar '" + grammarInfo.getAbsoluteGrammarFile() + "'");
    final JJDocProcessor jjp = new JJDocProcessor(getLog());
    jjp.inputFile = grammarInfo.getAbsoluteGrammarFile();
    jjp.cmdLineArgs = jjdocCmdLineArgs;
    jjp.run();
    grammarInfo.setMainGeneratedFile(jjp.outputFileName);
  }
  
  @Override
  public String getName(final Locale locale) {
    return getBundle(locale).getString("report.jjdoc.name");
  }
  
  @Override
  protected File[] getProcessorOutputDirectories() {
    return jjdab.processorOutputDirectories;
  }
  
  @Override
  public String getDescription(final Locale locale) {
    return getBundle(locale).getString("report.jjdoc.short.description");
  }
  
  @Override
  protected String getOutputDirectory() {
    return jjdocReportsDirectory.getAbsolutePath();
  }
  
  @Override
  public String getOutputPath() {
    return jjdocReportsPage;
  }
  
  /**
   * @deprecated Use {@link #getOutputPath()} instead.
   * @see org.apache.maven.reporting.MavenReport#getOutputName()
   * @return The name of the main report file.
   */
  @Deprecated
  @Override
  public String getOutputName() {
    return getOutputPath();
  }
  
  //  /**
  //   * The JJDoc output file will have a <code>.html</code> or <code>.txt</code> extension depending
  //   * on the value of the parameters {@link #text} and {@link #bnf}.
  //   *
  //   * @return The file extension (including the leading period) to be used for the JJDoc output
  //   *         files.
  //   */
  //  private String getOutputFileExtension() {
  //    if (Boolean.TRUE.equals(text) || Boolean.TRUE.equals(bnf)) {
  //      return ".txt";
  //    } else {
  //      return ".html";
  //    }
  //  }
  
  @Override
  protected String getGrammarFileEncoding() {
    return (jjdab.grammarFileEncodingOptionValue == null)
        ? AbstractArgumentsBean.defaultGrammarFileEncoding
        : jjdab.grammarFileEncodingOptionValue;
  }
  
  @Override
  protected void createReportHeader(final ResourceBundle bundle, final Sink sink) {
    sink.head();
    sink.title();
    sink.text(bundle.getString("report.jjdoc.title"));
    sink.title_();
    sink.head_();
    
    sink.body();
    
    sink.section1();
    sink.sectionTitle1();
    sink.text(bundle.getString("report.jjdoc.title"));
    sink.sectionTitle1_();
    sink.text(bundle.getString("report.jjdoc.description"));
    sink.section1_();
    
    sink.lineBreak();
    sink.table();
    sink.tableRows();
    sink.tableRow();
    sink.tableHeaderCell();
    sink.text(bundle.getString("report.jjdoc.table.heading"));
    sink.tableHeaderCell_();
    sink.tableRow_();
  }
  
  @Override
  protected void createReportFooter(final Sink sink) {
    sink.tableRows_();
    sink.table_();
    sink.body_();
  }
  
  @Override
  protected void createReportLink(final Sink sink, final GrammarInfo grammarInfo) {
    sink.tableRow();
    sink.tableCell();
    final String jjdocFile = grammarInfo.getMainGeneratedFile();
    getLog().debug("jjdocFile = '" + jjdocFile + "'");
    sink.link(new File(jjdocFile).toURI().getPath());
    final File sourceDirectory = grammarInfo.getSourceDirectory();
    getLog().debug("sourceDirectory = '" + sourceDirectory.getName() + "', '"
        + sourceDirectory.getAbsoluteFile() + "', '"
        + sourceDirectory.getAbsoluteFile().toURI().toString() + "'");
    final File grammarFile = new File(sourceDirectory, grammarInfo.grammarFile);
    getLog().debug("grammarFile = '" + grammarFile.getName() + "'");
    String grammarFileRelativePath = sourceDirectory.toURI().relativize(grammarFile.toURI())
        .toString();
    getLog().debug("grammarFileRelativePath = '" + grammarFileRelativePath + "'");
    if (grammarFileRelativePath.startsWith("/")) {
      grammarFileRelativePath = grammarFileRelativePath.substring(1);
    }
    sink.text(grammarFileRelativePath);
    sink.link_();
    sink.tableCell_();
    sink.tableRow_();
    getLog().debug("createReportLink '" + jjdocFile + "', '" + grammarFileRelativePath + "'");
  }
  
  @Override
  protected ResourceBundle getBundle(final Locale locale) {
    return ResourceBundle.getBundle("jjdoc-report", locale, getClass().getClassLoader());
  }
}