JFlexMojo.java
/*
* JFlex Maven3 plugin
* Copyright (c) 2007-2017 R��gis D��camps <decamps@users.sf.net>
* SPDX-License-Identifier: BSD-3-Clause
*/
package jflex.maven.plugin.jflex;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import jflex.core.OptionUtils;
import jflex.generator.LexGenerator;
import jflex.option.Options;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
/**
* Generates lexical scanners from one or more <a href="http://jflex.de/">JFlex</a> grammar files.
*
* @author R��gis D��camps (decamps@users.sf.net)
*/
@Mojo(name = "generate", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = false)
public class JFlexMojo extends AbstractMojo {
/** Name of the directory where to look for jflex files by default. */
private static final String SRC_MAIN_JFLEX = "src/main/jflex";
@Parameter(property = "project", required = true, readonly = true)
private MavenProject project;
// cannot use {@value SRC_MAIN_JFLEX} because Maven site goals.html
// is kept raw.
/**
* List of grammar definitions to run the JFlex parser generator on. Each path may either specify
* a single grammar file or a directory. Directories will be recursively scanned for files with
* one of the following extensions: ".jflex", ".flex", ".jlex" or ".lex". By default, all files in
* {@code src/main/jflex} will be processed.
*
* @see #SRC_MAIN_JFLEX
*/
@Parameter private File[] lexDefinitions;
/** Name of the directory into which JFlex should generate the parser. */
@Parameter(defaultValue = "${project.build.directory}/generated-sources/jflex")
private File outputDirectory;
/**
* The granularity in milliseconds of the last modification date for testing whether a source
* needs regeneration.
*/
@Parameter(property = "lastModGranularityMs", defaultValue = "0")
private int staleMillis;
/** Whether source code generation should be verbose. */
@Parameter(defaultValue = "false")
private boolean verbose;
/** Whether a warning will be logged when there are unused macros. */
@Parameter(defaultValue = "true")
private boolean unusedWarning;
/** Whether to dump full debug information. */
@Parameter(defaultValue = "false")
private boolean dump;
/**
* Whether to produce graphviz .dot files for the generated automata. This feature is
* EXPERIMENTAL.
*/
@Parameter(defaultValue = "false")
private boolean dot;
/** Use external skeleton file. */
@Parameter private File skeleton;
/** Strict JLex compatibility. */
@Parameter(defaultValue = "false")
private boolean jlex;
/** The generation method to use for the scanner. The only valid value is {@code pack}. */
@Parameter(defaultValue = "pack")
private String generationMethod = "pack"; // NOPMD
/** A flag whether to perform the DFA minimization step during scanner generation. */
@Parameter(defaultValue = "true")
private boolean minimize = true; // NOPMD
/**
* A flag whether to enable the generation of a backup copy if the generated source file already
* exists.
*/
@Parameter(defaultValue = "true")
private boolean backup = true; // NOPMD
/**
* If true, the dot (.) metachar matches [^\n] instead of [^\n\r\u000B\u000C\u0085\u2028\u2029].
*/
@Parameter(defaultValue = "false")
private boolean legacyDot = false; // NOPMD
/**
* The name of the character encoding for reading lexer specifications. Uses JVM default encoding
* if unset.
*/
@Parameter(defaultValue = "")
private String encodingName = ""; // NOPMD
/**
* Generate java parsers from lexer definition files.
*
* <p>This methods is checks parameters, sets options and calls JFlex.Main.generate()
*/
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
this.outputDirectory = getAbsolutePath(this.outputDirectory);
if (outputDirectory == null) {
throw new MojoExecutionException("outputDirectory is null");
}
// compiling the generated source in target/generated-sources/ is
// the whole point of this plugin compared to running the ant plugin
project.addCompileSourceRoot(outputDirectory.getPath());
List<File> filesIt;
if (lexDefinitions == null) {
// use default lexfiles if none provided
getLog().debug("Use lexer files found in (default) " + SRC_MAIN_JFLEX);
filesIt = new ArrayList<>();
File defaultDir = getAbsolutePath(new File(SRC_MAIN_JFLEX));
if (defaultDir.isDirectory()) {
filesIt.add(defaultDir);
}
} else {
// use arguments provided in the plugin configuration
filesIt = Arrays.asList(lexDefinitions);
getLog()
.debug(
"Parsing "
+ lexDefinitions.length
+ " jflex files or directories given in configuration");
}
// process all lexDefinitions
for (File lexDefinition : filesIt) {
lexDefinition = getAbsolutePath(lexDefinition);
parseLexDefinition(lexDefinition);
}
}
/**
* Generates java code of a parser from a lexer file.
*
* <p>If the {@code lexDefinition} is a directory, process all lexer files contained within.
*
* @param lexDefinition Lexer definiton file or directory to process.
* @throws MojoFailureException if the file is not found.
* @throws MojoExecutionException if file could not be parsed
*/
private void parseLexDefinition(File lexDefinition)
throws MojoFailureException, MojoExecutionException {
assert lexDefinition.isAbsolute() : lexDefinition;
if (lexDefinition.isDirectory()) {
// recursively process files contained within
getLog().debug("Processing lexer files found in " + lexDefinition);
Iterable<File> files =
Iterables.filter(
Files.fileTraverser().depthFirstPreOrder(lexDefinition),
new ExtensionPredicate("jflex", "jlex", "lex", "flex"));
for (File lexFile : files) {
parseLexFile(lexFile);
}
} else {
parseLexFile(lexDefinition);
}
}
private void parseLexFile(File lexFile) throws MojoFailureException, MojoExecutionException {
assert lexFile.isAbsolute() : lexFile;
getLog().debug("Generating Java code from " + lexFile.getName());
SpecInfo specInfo = findSpecInfo(lexFile);
checkParameters(lexFile);
// set destination directory
File generatedFile = new File(outputDirectory, specInfo.getOutputFilename());
// generate only if needs to
long generatedLastModified = generatedFile.lastModified();
if (lexFile.lastModified() - generatedLastModified <= this.staleMillis
&& latestModified(specInfo.includedFiles) - generatedLastModified <= this.staleMillis) {
getLog().info(" " + generatedFile.getName() + " is up to date.");
getLog().debug("StaleMillis = " + staleMillis + "ms");
return;
}
// set options. Very strange that JFlex expects this in a static way.
OptionUtils.setDefaultOptions();
OptionUtils.setDir(generatedFile.getParentFile());
Options.setRootDirectory(project.getBasedir());
Options.dump = dump;
Options.verbose = verbose;
OptionUtils.set_unused_warning(unusedWarning);
Options.dot = dot;
Options.legacy_dot = legacyDot;
if (skeleton != null) {
OptionUtils.setSkeleton(skeleton);
}
Options.jlex = jlex;
Options.no_minimize = !minimize; // NOPMD
Options.no_backup = !backup; // NOPMD
if (!Objects.equals("pack", generationMethod)) {
throw new MojoExecutionException("Illegal generation method: " + generationMethod);
}
if (!isNullOrEmpty(encodingName)) {
try {
OptionUtils.setEncoding(encodingName);
} catch (Exception e) {
throw new MojoExecutionException(e.getMessage());
}
}
try {
new LexGenerator(lexFile).generate();
getLog().info(" generated " + generatedFile);
} catch (Exception e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
private SpecInfo findSpecInfo(File lexFile) throws MojoFailureException {
try {
return LexSimpleAnalyzerUtils.guessSpecInfo(lexFile);
} catch (FileNotFoundException e) {
throw new MojoFailureException(e.getMessage(), e);
} catch (IOException e) {
return new SpecInfo(LexSimpleAnalyzerUtils.DEFAULT_NAME, /*packageName=*/ "");
}
}
/**
* Check parameter lexFile.
*
* <p>Must not be {@code null} and file must exist.
*
* @param lexFile input file to check.
* @throws MojoExecutionException in case of error
*/
private void checkParameters(File lexFile) throws MojoExecutionException {
if (lexFile == null) {
throw new MojoExecutionException(
"<lexDefinition> is empty. Please define input file with"
+ " <lexDefinition>input.jflex</lexDefinition>");
}
if (!lexFile.isFile()) {
throw new MojoExecutionException("Input file does not exist: " + lexFile);
}
}
/**
* Converts the specified path argument into an absolute path. If the path is relative like
* "src/main/jflex", it is resolved against the base directory of the project (in constrast,
* File.getAbsoluteFile() would resolve against the current directory which may be different,
* especially during a reactor build).
*
* @param path The path argument to convert, may be {@code null}.
* @return The absolute path corresponding to the input argument.
*/
private File getAbsolutePath(File path) {
if (path == null || path.isAbsolute()) {
return path;
}
return new File(this.project.getBasedir().getAbsolutePath(), path.getPath());
}
/**
* Determines the highest {@link File#lastModified()} value among the specified {@code
* includedFiles}, which are resolved relative to the specified {@code parent} directory.
*
* @return the latest value -- or 0 if the list is empty, if no files exist, or if I/O exceptions
* prevent getting any values
*/
private static long latestModified(Set<File> includedFiles) {
long result = 0;
for (File file : includedFiles) {
result = Math.max(file.lastModified(), result);
}
return result;
}
static class ExtensionPredicate implements Predicate<File> {
final ImmutableSet<String> extensions;
ExtensionPredicate(ImmutableSet<String> extensions) {
this.extensions = extensions;
}
ExtensionPredicate(String... extensions) {
this(ImmutableSet.copyOf(extensions));
}
@Override
public boolean apply(File file) {
return extensions.contains(Files.getFileExtension(file.getName()));
}
}
}