/*
* Copyright (C) 2011 Laurent Caillette
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.novelang.build;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.security.Permission;
import java.util.regex.Pattern;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Calls ANTLR parser generator.
* <p>
* Previous call from Ant script was starting a JVM with its own {@code user.dir} system property
* set to the directory containing grammar file. This new JVM induced a small little overhead that
* is avoided now as this class calls {@code org.antlr.Tool} directly.
* <p>
* Since ANTLR-3.1.1 doesn't generate correct headers (for both parser and lexer), there is
* a replacement of the first import declaration, by correct package declaration plus expected
* import.
* <p>
* Because {@code org.antlr.Tool} calls {@code System.exit} at the end, this class wraps the
* call in a block with a {@code SecurityManager} preventing from VM termination.
* <p>
* Because {@code org.antlr.Tool} directly writes errors on {@code System.err}, this one is
* trapped temporarily.
*
*
* @author Laurent Caillette
*/
public class AntlrGenerator extends GrammarBasedJavaGenerator {
/**
* Too bad, on't use Novelang log because of build structure.
*/
private static final Logger LOGGER = LoggerFactory.getLogger( AntlrGenerator.class ) ;
private static final String FIRST_IMPORT = "import novelang.parser.antlr.ProblemDelegate ;" ;
public AntlrGenerator(
final File grammarFile,
final String packageName,
final String className,
final File targetDirectory
) throws IOException
{
super( grammarFile, packageName, className, targetDirectory ) ;
}
@java.lang.Override
public void generate() throws IOException {
final File targetDirectory = getTargetFile().getParentFile() ;
createDirectory( targetDirectory ) ;
createDirectory( getGrammarFile() ) ;
final String[] arguments = {
"-trace",
"-fo",
targetDirectory.getCanonicalPath(),
getGrammarFile().getAbsolutePath()
} ;
LOGGER.info( "Command line: {}", Lists.newArrayList( arguments ) ) ;
final SystemErrorStreamTrapper systemErrorStreamTrapper = new SystemErrorStreamTrapper() ;
systemErrorStreamTrapper.install() ;
forbidSystemExitCall() ;
try {
try {
LOGGER.info( "Running ANTLR..." ) ;
org.antlr.Tool.main( arguments ) ;
LOGGER.info( "ANTLR ran successfully." ) ;
} catch( ExitTrappedException e ) {
// Do nothing, just prevents org.antlr.Tool from stopping the VM.
} finally {
enableSystemExitCall() ;
}
checkAntlrToolOutput( systemErrorStreamTrapper.getOutput() ) ;
} finally {
systemErrorStreamTrapper.uninstall() ;
}
fixPackageDeclaration( targetDirectory, getClassName(), "Parser" ) ;
fixPackageDeclaration( targetDirectory, getClassName(), "Lexer" ) ;
}
private void fixPackageDeclaration(
final File directory,
final String radix,
final String suffix
) throws IOException {
final File javaFile = new File( directory, radix + suffix + JAVA_EXTENSION ) ;
LOGGER.info( "Fixing package declaration for file: {}", javaFile.getAbsolutePath() ) ;
final String javaWithoutImports = IOUtils.toString( new FileInputStream( javaFile) ) ;
final String javaWithImports = javaWithoutImports.replaceAll(
Pattern.quote( FIRST_IMPORT ),
"package " + getPackageName() + " ;\n" + FIRST_IMPORT
) ;
final FileOutputStream fileOutputStream = new FileOutputStream( javaFile ) ;
IOUtils.write( javaWithImports, fileOutputStream ) ;
fileOutputStream.flush() ;
fileOutputStream.close() ;
}
@java.lang.Override
protected String generateCode() throws IOException {
throw new UnsupportedOperationException( "Don't call this method" ) ;
}
private static final String ANTLR_OUTPUT_INTRODUCTION = "ANTLR Parser Generator Version 3.1.1";
/**
* Checks ANTLR output on the console. This is an easy way to ensure we've been running
* ANTLR as expected.
*/
private void checkAntlrToolOutput( final String output ) {
final String antlrIntroductoryLine = ANTLR_OUTPUT_INTRODUCTION ;
if( output.startsWith( antlrIntroductoryLine ) ) {
final int introductoryLineLength = antlrIntroductoryLine.length();
if( output.length() > introductoryLineLength + 2 ) {
throw new RuntimeException( "\n" + output.substring( introductoryLineLength ) ) ;
}
} else {
throw new IllegalStateException(
"ANTLR output doesn't start with expected string: \n" + antlrIntroductoryLine +
"\nInstead got:\n" + output
) ;
}
}
// =======================
// Forbid System.exit call
// =======================
private static class ExitTrappedException extends SecurityException { }
private static void forbidSystemExitCall() {
final SecurityManager securityManager = new SecurityManager() {
@java.lang.Override
public void checkPermission( final Permission permission ) {
final String permissionName = permission.getName() ;
if( permissionName.startsWith( "exitVM" ) ) {
LOGGER.debug( "Checking permission " + permissionName ) ;
throw new ExitTrappedException() ;
}
}
} ;
System.setSecurityManager( securityManager ) ;
}
private static void enableSystemExitCall() {
System.setSecurityManager( null ) ;
}
// ======================
// Trap System.err output
// ======================
private class SystemErrorStreamTrapper {
private PrintStream initialPrintStream ;
private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream() ;
public void install() {
synchronized( System.class ) {
if( initialPrintStream != null ) {
throw new IllegalStateException( "Already installed" ) ;
}
initialPrintStream = System.err ;
System.setErr( new PrintStream( byteArrayOutputStream ) ) ;
}
}
public void uninstall() {
synchronized( System.class ) {
if( initialPrintStream == null ) {
throw new IllegalStateException( "Not installed or already uninstalled" ) ;
}
System.setErr( initialPrintStream ) ;
}
}
public String getOutput() {
return new String( byteArrayOutputStream.toByteArray() ) ;
}
}
}