AbstractPluginReport.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.io.IOException;
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.Parameter;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
/**
* Super class, managing common plugin-level parameters and methods, of the concrete mojos that
* generate report files, either:
* <ul>
* <li>directly as a standalone mojo (through the {@link #execute()} method), or</li>
* <li>indirectly from the web site generation (through the {@link #executeReport(Locale)}
* method).</li>
* </ul>
* Much of a copy of {@link AbstractPluginMojo}, but extending another maven class.
*
* <p>
* Each subclass manages a goal that
*
* <ul>
* <li>manages the corresponding processor(s) command line arguments (options) as maven parameters,
* with the help of corresponding beans, and
* <li>triggers execution of one or more processors.
* </ul>
*
* <p>
* One can set a specific log level for all classes of this plugin (without setting the global
* <code>-X</code> argument) through the (standard maven SLF4J Simple configuration) property<br>
* <code>-Dorg.slf4j.simpleLogger.log.org.javacc.mojo=debug</code>.<br>
*
* @since 3.8.0
* @author Maͫzͣaͬsͨ
* @see AbstractProcessor
* @see AbstractArgumentsBean
*/
public abstract class AbstractPluginReport extends AbstractMavenReport {
/**
* The set of Ant-like exclusion patterns used to prevent certain files from being processed.<br>
* By default, this set is empty such that no files are excluded.
*/
@Parameter //
protected String[] excludes;
/**
* The fail on plugin error flag.<br>
* It governs governs how the plugin will handle the errors it encountered on general
* configuration (i.e. parameters not related to grammars).<br>
* Possible values are <code>true</code> and <code>false</code>.<br>
* On an error: if set to <code>false</code>, the error message is displayed, the execution
* terminates, but the plugin does not report an error for the current execution (i.e. it
* continues with the next execution);<br>
* if set to <code>true</code> the error message is displayed, the execution terminates and the
* plugin reports an error for the build.
*/
@Parameter(property = "javacc.failOnPluginError", defaultValue = "true") //
protected Boolean failOnPluginError;
/**
* The fail on grammar error parameter.<br>
* It governs governs how the plugin will handle the errors it encountered while trying to read a
* grammar file to retrieve the parser name (and the parser package for languages that use it)
* (and in an execution the plugin may process zero, one or many grammars).<br>
* Possible values are <code>first</code>, <code>last</code> and <code>ignore</code>.<br>
* If set to <code>first</code>, the error message is displayed, the plugin stops processing other
* grammars and reports an error for the build;<br>
* if set to <code>last</code>, the error message is displayed, the plugin continues processing
* other grammars and at the end it reports an error for the build;<br>
* if set to <code>ignore</code> the error message(s) is(are) displayed but the plugin does not
* report an error for the current execution (i.e. it continues with the next execution).
*/
@Parameter(property = "javacc.failOnGrammarError", defaultValue = "first") //
protected String failOnGrammarError;
/**
* The fail on processor error parameter.<br>
* It governs how the plugin will handle the errors returned by the processor invocations and the
* plugin post-processor copy operations (in an execution the plugin may process zero, one or many
* grammars and invoke one or more processors for each).<br>
* Possible values are <code>first</code>, <code>last</code> and <code>ignore</code>.<br>
* If set to <code>first</code>, the error message is displayed, the plugin stops processing other
* grammars and reports an error for the build;<br>
* if set to <code>last</code>, the error message is displayed, the plugin continues processing
* other grammars and at the end it reports an error for the build;<br>
* if set to <code>ignore</code> the error message(s) is(are) displayed but the plugin does not
* report an error for the current execution (i.e. it continues with the next execution).
*/
@Parameter(property = "javacc.failOnProcessorError", defaultValue = "first") //
protected String failOnProcessorError;
/**
* The set of Ant-like inclusion patterns used to select files from the source directory for
* processing.<br>
* By default, the pattern is:<br>
* <code>**/*.jj</code>, <code>**/*.jjt</code>, <code>**/*.jtb</code> for the
* <code>jjdoc</code> goal.
*/
@Parameter //
protected String[] includes = null;
/**
* The skip processing flag.<br>
* If true, no goal will not be executed.
*/
@Parameter(property = "javacc.skip", defaultValue = "false") //
protected Boolean skip;
/**
* The directories where the grammar files are located.<br>
* They all must exist and be directories, otherwise a plugin error will be raised.<br>
* These directories will be recursively scanned for input files to pass to JavaCC.<br>
* Note that they will be under a common configuration; so if one wants a pom to process source
* directories with different configuration, he must configure multiple executions with different
* source directories.<br>
* The parameters <code>includes</code> and <code>excludes</code> can be used to select a subset
* of the files.<br>
* If not absolute paths, maven internals considers they ar relative to <code>${basedir}</code>
* and converts them accordingly to absolute paths (i.e. <code>src/main/javacc</code> will be
* considered as <code>${basedir}/src/main/javacc</code>, but <code>/src/main/javacc</code> will
* be considered as an absolute path, usually leading to an error.<br>
* The default value, adequate for a user written grammar, but not for a generated (by a
* preprocessor) grammar, is the array:<br>
* <code>${basedir}/src/main/javacc</code>, <code>${basedir}/src/main/jjtree</code>,
* <code>${basedir}/src/main/jtb</code>.
*/
@Parameter(property = "javacc.sourceDirectories") //
protected List<File> sourceDirectories = null;
/**
* Abstract getter.
*
* @return the grammar file encoding option value, or the default encoding if no option, never
* <code>null</code>
*/
protected abstract String getGrammarFileEncoding();
/**
* Flag telling if initialization has been performed.<br>
* Necessary, as {@link #canGenerateReport()} and {@link #executeReport(Locale)} can be called
* together on the same mojo instance, {@link #executeReport(Locale)} directly by the site plugin
* goal and or indirectly from the subclass mojo goal through (bottom->up)
* {@link #generate(Sink, org.apache.maven.doxia.sink.SinkFactory, Locale)} /
* {@link #reportToMarkup()} or {@link #reportToSite()} / {@link #execute()}, which itself calls
* {@link #canGenerateReport()}.
*/
private boolean initDone = false;
/**
* Initialize the mojo.
*
* @throws PluginException - for an error in initialization
*/
public void initialize() throws PluginException {
checkOptions();
initProcessor();
initDone = true;
}
@Override
public boolean canGenerateReport() {
getLog().debug("Entering canGenerateReport()");
if (skip) {
getLog().info("Skipping processing as requested");
return false;
}
try {
if (!initDone) {
initialize();
}
}
catch (final PluginException e) {
try {
// failOnPluginError == 'true' will throw a MavenReportException
handlePluginException(e);
// failOnPluginError == 'false' will continue here
}
catch (final MavenReportException mre) {
// in both cases we return false
}
return false;
}
for (final File dir : sourceDirectories) {
final String[] files = dir.list();
if (files != null && files.length > 0) {
getLog().debug("canGenerateReport() on sourceDirectories '"
+ displayDirectories(sourceDirectories) + "' returns true");
return true;
}
}
getLog().debug("canGenerateReport() on sourceDirectories '"
+ displayDirectories(sourceDirectories) + "' returns false");
return false;
}
@Override
public void executeReport(final Locale locale) throws MavenReportException {
getLog().debug(
"Entering executeReport(final Locale locale), locale is '" + locale.getDisplayName() + "'");
if (skip) {
getLog().info("Skipping processing as requested");
return;
}
try {
if (!initDone) {
initialize();
}
}
catch (final PluginException e) {
// failOnPluginError == 'true' will throw a MavenReportException
handlePluginException(e);
// failOnPluginError == 'false' will continue here
return;
}
final Sink sink = getSink();
createReportHeader(getBundle(locale), sink);
List<GrammarInfo> grammarInfos = null;
try {
grammarInfos = scanForGrammars();
}
catch (final GrammarException e) {
// 'ignore' for failOnGrammarError should not lead to throwing a GrammarException
// 'first' and 'last' for failOnGrammarError must lead to throw a MojoExecutionException
getLog().error(e.getMessage() + " while failOnGrammarError is '" + failOnGrammarError + "'");
throw new MavenReportException(e.getMessage() /*, e*/);
}
if (grammarInfos == null //
|| grammarInfos.size() == 0) {
String msg = "No grammars to process in source directories '";
msg += displayDirectories(sourceDirectories);
msg += "'";
getLog().info(msg);
return;
}
int nb = 0;
for (final GrammarInfo gi : grammarInfos) {
try {
processGrammar(gi);
}
catch (final ProcessorException e) {
handleProcessorException(e);
}
createReportLink(sink, gi);
nb++;
}
createReportFooter(sink);
sink.flush();
sink.close();
getLog().info("Processed " + nb + " grammar(s) successfully and " + (grammarInfos.size() - nb)
+ " with errors");
if (processorErrorToIgnore) {
// failOnProcessorError == 'ignore'
return;
} else if (processorErrorFailure) {
// failOnProcessorError == 'last'
throw new MavenReportException(
"Returning a build error as encountered one or more processor exceptions");
}
}
/**
* Create the header and title for the HTML report page.
*
* @param bundle - the resource bundle with the text.
* @param sink The sink to write to the main report file.
*/
protected abstract void createReportHeader(ResourceBundle bundle, Sink sink);
/**
* Create the HTML footer for the report page.
*
* @param sink The sink to write the HTML report page.
*/
protected abstract void createReportFooter(Sink sink);
/**
* Create a table row containing a link to the JJDoc report for a grammar file.
*
* @param sink - the sink to write the report
* @param grammarInfo - the grammar file information
*/
protected abstract void createReportLink(Sink sink, GrammarInfo grammarInfo);
/**
* Get the resource bundle for the report text.
*
* @param locale - the locale to use for this report
* @return The resource bundle
*/
protected abstract ResourceBundle getBundle(Locale locale);
/** True to tell the caller to ignore the grammar processing error. */
private boolean processorErrorToIgnore = false;
/**
* True to tell {@link #execute()} to return a build failure on grammar processing 'last' error.
*/
private boolean processorErrorFailure = false;
/**
* Transforms the plugin configuration exception in a build error if the plugin is configured to
* fail on plugin configuration errors, otherwise just logs the error.
*
* @param e - the plugin configuration exception
* @throws MavenReportException if the plugin is configured to fail on plugin configuration error
*/
private void handlePluginException(final PluginException e) throws MavenReportException {
if (failOnPluginError) {
getLog().error(e.getMessage());
throw new MavenReportException(e.getMessage() /*, e*/);
} else {
getLog().error(e.getMessage());
getLog().info("Continuing to next execution as failOnPluginError is set to 'false'");
}
}
/**
* Transforms the grammar processing exception in a build error if the plugin is configured to
* fail on 'first' grammar processing error, otherwise logs the error and sets flags for 'last'
* and 'ignore' values.
*
* @param e - the grammar processing exception
* @throws MavenReportException if the plugin is configured to fail on grammar processing error
*/
private void handleProcessorException(final ProcessorException e) throws MavenReportException {
if ("first".equals(failOnProcessorError)) {
getLog().error(e.getMessage());
throw new MavenReportException(e.getMessage() /*, e*/);
} else if ("last".equals(failOnProcessorError)) {
processorErrorFailure = true;
getLog().error(e.getMessage());
getLog().info("Continuing current execution as failOnProcessorError is set to 'last'");
} else { // 'ignore'
processorErrorToIgnore = true;
getLog().error(e.getMessage());
getLog().info("Continuing to next execution as failOnProcessorError is set to 'ignore'");
}
}
/**
* Checks valid values for different options.
*
* @throws PluginException if sourceDirectory does not exist or is not a directory
*/
protected void checkOptions() throws PluginException {
/* sourceDirectories */
// here it can be null; if so it will be initialized to a default value by each processor;
// and if not null, maven internals seems to always convert relative paths to an absolute ones
getLog().debug("sourceDirectories is '" + displayDirectories(sourceDirectories) + "'");
if (sourceDirectories != null) {
for (final File dir : sourceDirectories) {
if (!dir.exists()) {
throw new PluginException("sourceDirectory '" + dir + "' does not exist");
} else if (!dir.isDirectory()) {
throw new PluginException("sourceDirectory '" + dir + "' is not a directory");
}
}
}
/* failOnPluginError */
getLog().debug("failOnPluginError is '" + failOnPluginError + "'");
/* failOnGrammarError */
switch (failOnGrammarError.toLowerCase()) {
case "first":
case "last":
case "ignore":
failOnGrammarError = failOnGrammarError.toLowerCase();
getLog().debug("failOnGrammarError is '" + failOnGrammarError + "'");
break;
default:
getLog().warn("invalid value '" + failOnGrammarError
+ "' for failOnGrammarError parameter; must be 'first', 'last' or 'ignore';"
+ " kept to default 'first'");
failOnGrammarError = "first";
break;
}
/* failOnProcessorError */
switch (failOnProcessorError.toLowerCase()) {
case "first":
case "last":
case "ignore":
failOnProcessorError = failOnProcessorError.toLowerCase();
getLog().debug("failOnProcessorError is '" + failOnProcessorError + "'");
break;
default:
getLog().warn("invalid value '" + failOnProcessorError
+ "' for failOnProcessorError parameter; must be 'first', 'last' or 'ignore';"
+ " kept to default 'first'");
failOnProcessorError = "first";
break;
}
}
/**
* Initializes (some default values specific to the processor(s) and those ones).
*
* @throws PluginException if invalid option value
*/
protected abstract void initProcessor() throws PluginException;
/**
* Scans the source directories for grammar files which need processing.
*
* @return a list of grammar infos describing the found grammar files, may be <code>null</code> if
* no grammar in the directory
* @throws GrammarException if some grammar file could not be read or parsed
*/
protected List<GrammarInfo> scanForGrammars() throws GrammarException {
getLog().debug("Scanning for grammars in '" + displayDirectories(sourceDirectories) + "'");
final GrammarDirectoryScanner gds = //
new GrammarDirectoryScanner(getLog(), null, null);
gds.dsSetExcludes(excludes);
gds.dsSetIncludes(includes);
gds.setJarsLastTS(-1L);
gds.setOutputDirectories(null);
gds.setTimestampDeltaMs(-1L);
List<GrammarInfo> grammarInfos = null;
for (final File dir : sourceDirectories) {
gds.dsSetBasedir(dir);
final List<GrammarInfo> grInfos = gds.scanForGrammars(failOnGrammarError);
if (grInfos != null && grInfos.size() > 0) {
if (grammarInfos == null) {
grammarInfos = grInfos;
} else {
grammarInfos.addAll(grInfos);
}
}
}
getLog().debug("Found grammars: "
+ (grammarInfos == null ? "none" : Arrays.toString(grammarInfos.toArray())));
return grammarInfos;
}
/**
* Abstract getter.
*
* @return the array of absolute paths to the directories where the (current) processor will
* output its generated files, never <code>null</code>
*/
protected abstract File[] getProcessorOutputDirectories();
/**
* Tells the subclass to process the specified grammar file (it may execute one or more
* processors).
*
* @param grammarInfo - the grammar info describing the grammar file to process, must not be
* <code>null</code>
* @throws ProcessorException if the invocation of the processor(s) failed or if the processor(s)
* reported a non-zero exit code or if the generated files could not be copied
*/
protected abstract void processGrammar(GrammarInfo grammarInfo) throws ProcessorException;
/**
* @param directories - a list of directories
* @return the string of the comma separated list of directories
*/
protected static String displayDirectories(final List<File> directories) {
if (directories == null) {
return null;
}
String msg = "";
for (int i = 0; i < directories.size(); i++) {
try {
msg += directories.get(i).getCanonicalPath();
}
catch (final IOException e) {
msg += "IOException on element of sourceDirectories";
}
if (i < directories.size() - 1) {
msg += ", ";
}
}
return msg;
}
}