View Javadoc

1   /*
2    [The "BSD license"]
3    Copyright (c) 2012 Terence Parr
4    Copyright (c) 2012 Sam Harwell
5    All rights reserved.
6   
7    Redistribution and use in source and binary forms, with or without
8    modification, are permitted provided that the following conditions
9    are met:
10   1. Redistributions of source code must retain the above copyright
11      notice, this list of conditions and the following disclaimer.
12   2. Redistributions in binary form must reproduce the above copyright
13      notice, this list of conditions and the following disclaimer in the
14      documentation and/or other materials provided with the distribution.
15   3. The name of the author may not be used to endorse or promote products
16      derived from this software without specific prior written permission.
17  
18   THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19   IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21   IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29  
30  package org.antlr.mojo.antlr4;
31  
32  import org.antlr.v4.Tool;
33  import org.antlr.v4.codegen.CodeGenerator;
34  import org.antlr.v4.runtime.misc.MultiMap;
35  import org.antlr.v4.runtime.misc.NotNull;
36  import org.antlr.v4.runtime.misc.Utils;
37  import org.antlr.v4.tool.Grammar;
38  import org.apache.maven.plugin.AbstractMojo;
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugin.MojoFailureException;
41  import org.apache.maven.plugin.logging.Log;
42  import org.apache.maven.plugins.annotations.Component;
43  import org.apache.maven.plugins.annotations.LifecyclePhase;
44  import org.apache.maven.plugins.annotations.Mojo;
45  import org.apache.maven.plugins.annotations.Parameter;
46  import org.apache.maven.plugins.annotations.ResolutionScope;
47  import org.apache.maven.project.MavenProject;
48  import org.codehaus.plexus.compiler.util.scan.InclusionScanException;
49  import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
50  import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
51  import org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping;
52  import org.codehaus.plexus.compiler.util.scan.mapping.SuffixMapping;
53  import org.sonatype.plexus.build.incremental.BuildContext;
54  
55  import java.io.BufferedWriter;
56  import java.io.File;
57  import java.io.IOException;
58  import java.io.OutputStream;
59  import java.io.OutputStreamWriter;
60  import java.io.StringWriter;
61  import java.io.Writer;
62  import java.net.URI;
63  import java.util.ArrayList;
64  import java.util.Arrays;
65  import java.util.Collections;
66  import java.util.HashSet;
67  import java.util.List;
68  import java.util.Map;
69  import java.util.Set;
70  
71  /**
72   * Parses ANTLR 4 grammar files {@code *.g4} and transforms them into Java
73   * source files.
74   *
75   * @author Sam Harwell
76   */
77  @Mojo(
78  	name = "antlr4",
79  	defaultPhase = LifecyclePhase.GENERATE_SOURCES,
80  	requiresDependencyResolution = ResolutionScope.COMPILE,
81  	requiresProject = true)
82  public class Antlr4Mojo extends AbstractMojo {
83  
84      // First, let's deal with the options that the ANTLR tool itself
85      // can be configured by.
86      //
87  
88      /**
89       * If set to true then the ANTLR tool will generate a description of the ATN
90       * for each rule in <a href="http://www.graphviz.org">Dot format</a>.
91       */
92      @Parameter(property = "antlr4.atn", defaultValue = "false")
93      protected boolean atn;
94  
95  	/**
96  	 * specify grammar file encoding; e.g., euc-jp
97  	 */
98  	@Parameter(property = "project.build.sourceEncoding")
99  	protected String encoding;
100 
101 	/**
102 	 * Generate parse tree listener interface and base class.
103 	 */
104 	@Parameter(property = "antlr4.listener", defaultValue = "true")
105 	protected boolean listener;
106 
107 	/**
108 	 * Generate parse tree visitor interface and base class.
109 	 */
110 	@Parameter(property = "antlr4.visitor", defaultValue = "false")
111 	protected boolean visitor;
112 
113 	/**
114 	 * Treat warnings as errors.
115 	 */
116 	@Parameter(property = "antlr4.treatWarningsAsErrors", defaultValue = "false")
117 	protected boolean treatWarningsAsErrors;
118 
119 	/**
120 	 * Use the ATN simulator for all predictions.
121 	 */
122 	@Parameter(property = "antlr4.forceATN", defaultValue = "false")
123 	protected boolean forceATN;
124 
125 	/**
126 	 * A list of grammar options to explicitly specify to the tool. These
127 	 * options are passed to the tool using the
128 	 * <code>-D&lt;option&gt;=&lt;value&gt;</code> syntax.
129 	 */
130 	@Parameter
131 	protected Map<String, String> options;
132 
133 	/**
134 	 * A list of additional command line arguments to pass to the ANTLR tool.
135 	 */
136 	@Parameter
137 	protected List<String> arguments;
138 
139     /* --------------------------------------------------------------------
140      * The following are Maven specific parameters, rather than specific
141      * options that the ANTLR tool can use.
142      */
143 
144 	/**
145 	 * Provides an explicit list of all the grammars that should be included in
146 	 * the generate phase of the plugin. Note that the plugin is smart enough to
147 	 * realize that imported grammars should be included but not acted upon
148 	 * directly by the ANTLR Tool.
149 	 * <p/>
150 	 * A set of Ant-like inclusion patterns used to select files from the source
151 	 * directory for processing. By default, the pattern
152 	 * <code>**&#47;*.g4</code> is used to select grammar files.
153 	 */
154     @Parameter
155     protected Set<String> includes = new HashSet<String>();
156     /**
157      * A set of Ant-like exclusion patterns used to prevent certain files from
158      * being processed. By default, this set is empty such that no files are
159      * excluded.
160      */
161     @Parameter
162     protected Set<String> excludes = new HashSet<String>();
163     /**
164      * The current Maven project.
165      */
166     @Parameter(property = "project", required = true, readonly = true)
167     protected MavenProject project;
168 
169     /**
170      * The directory where the ANTLR grammar files ({@code *.g4}) are located.
171      */
172 	@Parameter(defaultValue = "${basedir}/src/main/antlr4")
173     private File sourceDirectory;
174 
175     /**
176      * Specify output directory where the Java files are generated.
177      */
178 	@Parameter(defaultValue = "${project.build.directory}/generated-sources/antlr4")
179     private File outputDirectory;
180 
181     /**
182      * Specify location of imported grammars and tokens files.
183      */
184 	@Parameter(defaultValue = "${basedir}/src/main/antlr4/imports")
185     private File libDirectory;
186 
187 	@Component
188 	private BuildContext buildContext;
189 
190     public File getSourceDirectory() {
191         return sourceDirectory;
192     }
193 
194     public File getOutputDirectory() {
195         return outputDirectory;
196     }
197 
198     public File getLibDirectory() {
199         return libDirectory;
200     }
201 
202     void addSourceRoot(File outputDir) {
203         project.addCompileSourceRoot(outputDir.getPath());
204     }
205 
206     /**
207      * An instance of the ANTLR tool build
208      */
209     protected Tool tool;
210 
211     /**
212      * The main entry point for this Mojo, it is responsible for converting
213      * ANTLR 4.x grammars into the target language specified by the grammar.
214      *
215      * @exception MojoExecutionException if a configuration or grammar error causes
216      * the code generation process to fail
217      * @exception MojoFailureException if an instance of the ANTLR 4 {@link Tool}
218      * cannot be created
219      */
220     @Override
221     public void execute() throws MojoExecutionException, MojoFailureException {
222 
223         Log log = getLog();
224 
225         if (log.isDebugEnabled()) {
226             for (String e : excludes) {
227                 log.debug("ANTLR: Exclude: " + e);
228             }
229 
230             for (String e : includes) {
231                 log.debug("ANTLR: Include: " + e);
232             }
233 
234             log.debug("ANTLR: Output: " + outputDirectory);
235             log.debug("ANTLR: Library: " + libDirectory);
236         }
237 
238 		if (!sourceDirectory.isDirectory()) {
239 			log.info("No ANTLR 4 grammars to compile in " + sourceDirectory.getAbsolutePath());
240 			return;
241 		}
242 
243         // Ensure that the output directory path is all in tact so that
244         // ANTLR can just write into it.
245         //
246         File outputDir = getOutputDirectory();
247 
248         if (!outputDir.exists()) {
249             outputDir.mkdirs();
250         }
251 
252 		// Now pick up all the files and process them with the Tool
253 		//
254 
255 		List<List<String>> argumentSets;
256         try {
257 			List<String> args = getCommandArguments();
258             argumentSets = processGrammarFiles(args, sourceDirectory);
259         } catch (InclusionScanException ie) {
260             log.error(ie);
261             throw new MojoExecutionException("Fatal error occured while evaluating the names of the grammar files to analyze", ie);
262         }
263 
264 		log.debug("Output directory base will be " + outputDirectory.getAbsolutePath());
265 		log.info("ANTLR 4: Processing source directory " + sourceDirectory.getAbsolutePath());
266 		for (List<String> args : argumentSets) {
267 			try {
268 				// Create an instance of the ANTLR 4 build tool
269 				tool = new CustomTool(args.toArray(new String[args.size()]));
270 			} catch (Exception e) {
271 				log.error("The attempt to create the ANTLR 4 build tool failed, see exception report for details", e);
272 				throw new MojoFailureException("Error creating an instanceof the ANTLR tool.", e);
273 			}
274 
275 			// Set working directory for ANTLR to be the base source directory
276 			tool.inputDirectory = sourceDirectory;
277 
278 			tool.processGrammarsOnCommandLine();
279 
280 			// If any of the grammar files caused errors but did nto throw exceptions
281 			// then we should have accumulated errors in the counts
282 			if (tool.getNumErrors() > 0) {
283 				throw new MojoExecutionException("ANTLR 4 caught " + tool.getNumErrors() + " build errors.");
284 			}
285 		}
286 
287         if (project != null) {
288             // Tell Maven that there are some new source files underneath the output directory.
289             addSourceRoot(this.getOutputDirectory());
290         }
291     }
292 
293 	private List<String> getCommandArguments() {
294 		List<String> args = new ArrayList<String>();
295 
296 		if (getOutputDirectory() != null) {
297 			args.add("-o");
298 			args.add(outputDirectory.getAbsolutePath());
299 		}
300 
301 		// Where do we want ANTLR to look for .tokens and import grammars?
302 		if (getLibDirectory() != null && getLibDirectory().isDirectory()) {
303 			args.add("-lib");
304 			args.add(libDirectory.getAbsolutePath());
305 		}
306 
307         // Next we need to set the options given to us in the pom into the
308         // tool instance we have created.
309 		if (atn) {
310 			args.add("-atn");
311 		}
312 
313 		if (encoding != null && !encoding.isEmpty()) {
314 			args.add("-encoding");
315 			args.add(encoding);
316 		}
317 
318 		if (listener) {
319 			args.add("-listener");
320 		}
321 		else {
322 			args.add("-no-listener");
323 		}
324 
325 		if (visitor) {
326 			args.add("-visitor");
327 		}
328 		else {
329 			args.add("-no-visitor");
330 		}
331 
332 		if (treatWarningsAsErrors) {
333 			args.add("-Werror");
334 		}
335 
336 		if (forceATN) {
337 			args.add("-Xforce-atn");
338 		}
339 
340 		if (options != null) {
341 			for (Map.Entry<String, String> option : options.entrySet()) {
342 				args.add(String.format("-D%s=%s", option.getKey(), option.getValue()));
343 			}
344 		}
345 
346 		if (arguments != null) {
347 			args.addAll(arguments);
348 		}
349 
350 		return args;
351 	}
352 
353     /**
354      *
355      * @param sourceDirectory
356      * @exception InclusionScanException
357      */
358     @NotNull
359     private List<List<String>> processGrammarFiles(List<String> args, File sourceDirectory) throws InclusionScanException {
360         // Which files under the source set should we be looking for as grammar files
361         SourceMapping mapping = new SuffixMapping("g4", Collections.<String>emptySet());
362 
363         // What are the sets of includes (defaulted or otherwise).
364         Set<String> includes = getIncludesPatterns();
365 
366         // Now, to the excludes, we need to add the imports directory
367         // as this is autoscanned for imported grammars and so is auto-excluded from the
368         // set of grammar fields we should be analyzing.
369         excludes.add("imports/**");
370 
371         SourceInclusionScanner scan = new SimpleSourceInclusionScanner(includes, excludes);
372         scan.addSourceMapping(mapping);
373         Set<File> grammarFiles = scan.getIncludedSources(sourceDirectory, null);
374 
375         if (grammarFiles.isEmpty()) {
376             getLog().info("No grammars to process");
377 			return Collections.emptyList();
378 		}
379 
380 		MultiMap<String, File> grammarFileByFolder = new MultiMap<String, File>();
381 		// Iterate each grammar file we were given and add it into the tool's list of
382 		// grammars to process.
383 		for (File grammarFile : grammarFiles) {
384 			if (!buildContext.hasDelta(grammarFile)) {
385 				continue;
386 			}
387 
388 			buildContext.removeMessages(grammarFile);
389 
390 			getLog().debug("Grammar file '" + grammarFile.getPath() + "' detected.");
391 
392 			String relPathBase = findSourceSubdir(sourceDirectory, grammarFile.getPath());
393 			String relPath = relPathBase + grammarFile.getName();
394 			getLog().debug("  ... relative path is: " + relPath);
395 
396 			grammarFileByFolder.map(relPathBase, grammarFile);
397 		}
398 
399 		List<List<String>> result = new ArrayList<List<String>>();
400 		for (Map.Entry<String, List<File>> entry : grammarFileByFolder.entrySet()) {
401 			List<String> folderArgs = new ArrayList<String>(args);
402 			if (!folderArgs.contains("-package") && !entry.getKey().isEmpty()) {
403 				folderArgs.add("-package");
404 				folderArgs.add(getPackageName(entry.getKey()));
405 			}
406 
407 			for (File file : entry.getValue()) {
408 				folderArgs.add(entry.getKey() + file.getName());
409 			}
410 
411 			result.add(folderArgs);
412 		}
413 
414 		return result;
415 	}
416 
417 	private static String getPackageName(String relativeFolderPath) {
418 		if (relativeFolderPath.contains("..")) {
419 			throw new UnsupportedOperationException("Cannot handle relative paths containing '..'");
420 		}
421 
422 		List<String> parts = new ArrayList<String>(Arrays.asList(relativeFolderPath.split("[/\\\\\\.]+")));
423 		while (parts.remove("")) {
424 			// intentionally blank
425 		}
426 
427 		return Utils.join(parts.iterator(), ".");
428 	}
429 
430     public Set<String> getIncludesPatterns() {
431         if (includes == null || includes.isEmpty()) {
432             return Collections.singleton("**/*.g4");
433         }
434         return includes;
435     }
436 
437     /**
438      * Given the source directory File object and the full PATH to a grammar,
439      * produce the path to the named grammar file in relative terms to the
440      * {@code sourceDirectory}. This will then allow ANTLR to produce output
441      * relative to the base of the output directory and reflect the input
442      * organization of the grammar files.
443      *
444      * @param sourceDirectory The source directory {@link File} object
445      * @param grammarFileName The full path to the input grammar file
446      * @return The path to the grammar file relative to the source directory
447      */
448     private String findSourceSubdir(File sourceDirectory, String grammarFileName) {
449         String srcPath = sourceDirectory.getPath() + File.separator;
450 
451         if (!grammarFileName.startsWith(srcPath)) {
452             throw new IllegalArgumentException("expected " + grammarFileName + " to be prefixed with " + sourceDirectory);
453         }
454 
455         File unprefixedGrammarFileName = new File(grammarFileName.substring(srcPath.length()));
456 		if (unprefixedGrammarFileName.getParent() == null) {
457 			return "";
458 		}
459 
460         return unprefixedGrammarFileName.getParent() + File.separator;
461     }
462 
463 	private final class CustomTool extends Tool {
464 
465 		public CustomTool(String[] args) {
466 			super(args);
467 			addListener(new Antlr4ErrorLog(this, buildContext, getLog()));
468 		}
469 
470 		@Override
471 		public void process(Grammar g, boolean gencode) {
472 			getLog().info("Processing grammar: " + g.fileName);
473 			super.process(g, gencode);
474 		}
475 
476 		@Override
477 		public Writer getOutputFileWriter(Grammar g, String fileName) throws IOException {
478 			if (outputDirectory == null) {
479 				return new StringWriter();
480 			}
481 			// output directory is a function of where the grammar file lives
482 			// for subdir/T.g4, you get subdir here.  Well, depends on -o etc...
483 			// But, if this is a .tokens file, then we force the output to
484 			// be the base output directory (or current directory if there is not a -o)
485 			//
486 			File outputDir;
487 			if ( fileName.endsWith(CodeGenerator.VOCAB_FILE_EXTENSION) ) {
488 				outputDir = new File(outputDirectory);
489 			}
490 			else {
491 				outputDir = getOutputDirectory(g.fileName);
492 			}
493 
494 			File outputFile = new File(outputDir, fileName);
495 			if (!outputDir.exists()) {
496 				outputDir.mkdirs();
497 			}
498 
499 			URI relativePath = project.getBasedir().toURI().relativize(outputFile.toURI());
500 			getLog().debug("  Writing file: " + relativePath);
501 			OutputStream outputStream = buildContext.newFileOutputStream(outputFile);
502 			return new BufferedWriter(new OutputStreamWriter(outputStream));
503 		}
504 	}
505 }