GrammarDirectoryScanner.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 org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.util.DirectoryScanner;
/**
* Scans source directories for JavaCC / JJTree / JTB grammar files, performing stale detection.
*
* @since 3.8.0
* @author Maͫzͣaͬsͨ
*/
class GrammarDirectoryScanner {
/** The logger. */
private final Log log;
/** The output language. */
private final Language language;
/** The grammar file encoding. */
private final String encoding;
/** The directory scanner used to scan the source directory for files. */
private final DirectoryScanner src_ds;
/** The directory scanner used to scan the target directory for files. */
private final DirectoryScanner tgt_ds;
/**
* The array of absolute path to the output directories used to detect stale target files by
* timestamp checking, may be <code>null</code> if no stale detection should be performed.
*/
private File[] outputDirectories;
/**
* The delta in milliseconds of the last modification timestamp for testing whether a grammar file
* needs recompilation because its main generated file is stale.
*/
private long timestampDeltaMs;
/**
* Creates a new grammar directory scanner.
*
* @param lg - the Log to use
* @param lang - the Language to generate for
* @param enc - the grammar file encoding
*/
public GrammarDirectoryScanner(final Log lg, final Language lang, final String enc) {
log = lg;
language = lang;
encoding = enc;
src_ds = new DirectoryScanner();
src_ds.setFollowSymlinks(true);
tgt_ds = new DirectoryScanner();
tgt_ds.setFollowSymlinks(true);
}
/** The most recent lastModified timestamp of the dependencies jars. */
private long jarsLastTS = 0;
/**
* Setter.
*
* @param ts - the most recent lastModified timestamp of the dependencies jars
*/
public void setJarsLastTS(final long ts) {
jarsLastTS = ts;
}
/**
* Setter.
*
* @param directories - the arrays of absolute path to the output directories used to detect stale
* target files by timestamp checking, should not be <code>null</code>.
*/
public void setOutputDirectories(final File[] directories) {
outputDirectories = directories;
}
/**
* Setter.
*
* @param milliseconds - the delta in milliseconds of the last modification timestamp
*/
public void setTimestampDeltaMs(final long milliseconds) {
timestampDeltaMs = milliseconds;
}
/**
* Sets the directory scanner with its base directory.<br>
* This directory must exist or the scanner will report an error.
*
* @param directory - the absolute path to the source directory to scan, must not be <code>null
* </code>
*/
public void dsSetBasedir(final File directory) {
src_ds.setBasedir(directory);
}
/**
* Sets the directory scanner with its inclusion pattern.
*
* @param includes - the set of Ant-like inclusion patterns, may be <code>null</code> to include
* all files
*/
public void dsSetIncludes(final String[] includes) {
src_ds.setIncludes(includes);
}
/**
* Sets the directory scanner with its exclusion patterns.
*
* @param excludes - the set of Ant-like exclusion patterns, may be <code>null</code> to exclude
* no files
*/
public void dsSetExcludes(final String[] excludes) {
src_ds.setExcludes(excludes);
src_ds.addDefaultExcludes();
}
/**
* Scans the source directory for grammar files that match at least one inclusion pattern but no
* exclusion pattern, performing timestamp checking to include grammars that are older than the
* dependent jars and optionally performing timestamp checking to exclude grammars whose
* corresponding parser files are up to date.
*
* @param failOnGrammarError - the plugin parameter failOnGrammarError value
* @return a list of grammar infos, may be <code>null</code> if no grammar in the directory, and
* may be empty if no stale grammar found
* @throws GrammarException if reading the grammar file failed, or if no parser name can be
* retrieved in the grammar
*/
public List<GrammarInfo> scanForGrammars(final String failOnGrammarError)
throws GrammarException {
// here we do not catch the IllegalStateException as basedir (previously set to sourceDirectory)
// has already (normally) be checked as not null, existing and being a directory
src_ds.scan();
final String[] includedFiles = src_ds.getIncludedFiles();
if (includedFiles.length == 0) {
return null;
}
final List<GrammarInfo> includedGrammars = new ArrayList<GrammarInfo>();
int nbError = 0;
for (final String includedFile : includedFiles) {
log.debug("IncludedFile = '" + includedFile + "'");
GrammarInfo grammarInfo = null;
try {
grammarInfo = new GrammarInfo(log, language, encoding, src_ds.getBasedir(), includedFile);
}
catch (final GrammarException e) {
if ("first".equals(failOnGrammarError)) {
// 'first': let it be handled above
throw e;
} else {
// 'last' or 'ignore': continue on next grammar
nbError++;
log.error(e.getMessage());
log.info(
"Continuing finding grammars as failOnGrammarError is set to 'last' or 'ignore'");
continue;
}
}
final File grammarFile = grammarInfo.getAbsoluteGrammarFile();
final long grammarTS = grammarFile.lastModified();
log.debug(
"LastModified timestamp of grammar file '" + grammarFile + "' is '" + grammarTS + "'");
if (timestampDeltaMs >= 0) {
// stale detection needed
for (final File outDir : outputDirectories) {
if (includedGrammars.contains(grammarInfo)) {
// (do not to include more than once a grammar)
break;
}
log.debug("Getting target files on '" + outDir + "'");
final File[] targetFiles = getTargetFile(outDir, grammarInfo);
if (targetFiles == null //
|| targetFiles.length == 0) {
// no generated file
includedGrammars.add(grammarInfo);
log.info(
"Grammar file '" + grammarFile + "' included as no existing main generated file");
} else {
for (final File targetFile : targetFiles) {
final long targetTS = targetFile.lastModified();
log.debug("LastModified timestamp of main generated file '" + targetFile + "' is '"
+ targetTS + "'");
if (targetTS + timestampDeltaMs < grammarTS) {
// grammar file more recent than generated file
includedGrammars.add(grammarInfo);
log.info("Grammar file '" + grammarFile
+ "' included as newer than main generated file '" + targetFile + "'");
break;
}
if (targetTS + timestampDeltaMs < jarsLastTS) {
// jar file more recent than target file
includedGrammars.add(grammarInfo);
log.info("Grammar file'" + grammarFile + "' included as main generated file '"
+ targetFile + "' older than dependent jar(s)");
break;
}
log.info("Grammar file '" + grammarFile + "' not included");
}
}
}
} else {
// no stale detection requested
includedGrammars.add(grammarInfo);
log.info("Grammar file '" + grammarFile + "' included as no stale detection requested");
}
}
if (nbError > 0) {
if ("last".equals(failOnGrammarError)) {
throw new GrammarException(
"Grammar reading error(s) encountered (see above), scan finished and leaving execution");
} else {
// failOnGrammarError == 'ignore'
log.info("Encountered " + nbError
+ " grammar reading errors, but ignored and continuing execution");
}
}
return includedGrammars;
}
/**
* Determines the main generated file corresponding to the specified grammar file.
*
* @param targetDirectory - the absolute path to the output directory for the target files, must
* not be <code>null</code>
* @param grammarInfo - the grammar info describing the grammar file, must not be <code>null
* </code>
* @return a file array with main generated file, may be <code>null</code>, may be empty
*/
protected File[] getTargetFile(final File targetDirectory, final GrammarInfo grammarInfo) {
if (!targetDirectory.exists()) {
log.debug("targetDirectory '" + targetDirectory + "' does not exist, no target file");
return null;
}
tgt_ds.setBasedir(targetDirectory);
tgt_ds.setIncludes(new String[] {
grammarInfo.getMainGeneratedFile()
});
tgt_ds.setExcludes(null);
// here we do not catch the IllegalStateException as targetDirectory
// has already (normally) be checked as not null, existing and being a directory
tgt_ds.scan();
final String[] includedFiles = tgt_ds.getIncludedFiles();
// well, we should get 0 or 1 result
final File[] targetFiles = new File[includedFiles.length];
int k = 0;
for (final String includedFile : includedFiles) {
targetFiles[k] = new File(targetDirectory, includedFile);
k++;
}
log.debug("targetFiles = '" + Arrays.toString(targetFiles) + "'");
return targetFiles;
}
}