package org.codehaus.plexus.compiler.javac;
/**
* The MIT License
*
* Copyright (c) 2005, The Codehaus
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
*
* Copyright 2004 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.Arrays;
import org.codehaus.plexus.compiler.AbstractCompiler;
import org.codehaus.plexus.compiler.CompilerConfiguration;
import org.codehaus.plexus.compiler.CompilerError;
import org.codehaus.plexus.compiler.CompilerException;
import org.codehaus.plexus.compiler.CompilerOutputStyle;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;
/**
* @plexus.component
* role="org.codehaus.plexus.compiler.Compiler"
* role-hint="javac"
*
* @author <a href="mailto:trygvis@inamo.no">Trygve Laugstøl</a>
* @author <a href="mailto:matthew.pocock@ncl.ac.uk">Matthew Pocock</a>
* @author Others
* @version $Id: JavacCompiler.java 8733 2010-08-29 14:04:38Z bentmann $
*/
public class JavacCompiler
extends AbstractCompiler
{
// see compiler.warn.warning in compiler.properties of javac sources
private static final String[] WARNING_PREFIXES = { "warning: ", "\u8b66\u544a: ", "\u8b66\u544a\uff1a " };
// see compiler.note.note in compiler.properties of javac sources
private static final String[] NOTE_PREFIXES = { "Note: ", "\u6ce8: ", "\u6ce8\u610f\uff1a " };
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
public JavacCompiler()
{
super( CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE,
".java",
".class",
null );
}
// ----------------------------------------------------------------------
// Compiler Implementation
// ----------------------------------------------------------------------
public List compile( CompilerConfiguration config )
throws CompilerException
{
File destinationDir = new File( config.getOutputLocation() );
if ( !destinationDir.exists() )
{
destinationDir.mkdirs();
}
String[] sourceFiles = getSourceFiles( config );
if ( ( sourceFiles == null ) || ( sourceFiles.length == 0 ) )
{
return Collections.EMPTY_LIST;
}
if ( ( getLogger() != null ) && getLogger().isInfoEnabled() )
{
getLogger().info( "Compiling " + sourceFiles.length + " " +
"source file" + ( sourceFiles.length == 1 ? "" : "s" ) +
" to " + destinationDir.getAbsolutePath() );
}
String[] args = buildCompilerArguments( config, sourceFiles );
List messages;
if ( config.isFork() )
{
String executable = config.getExecutable();
if ( StringUtils.isEmpty( executable ) )
{
try
{
executable = getJavacExecutable();
}
catch ( IOException e )
{
getLogger().warn( "Unable to autodetect 'javac' path, using 'javac' from the environment." );
executable = "javac";
}
}
messages = compileOutOfProcess( config, executable, args );
}
else
{
messages = compileInProcess( args );
}
return messages;
}
public String[] createCommandLine( CompilerConfiguration config )
throws CompilerException
{
return buildCompilerArguments( config, getSourceFiles( config ) );
}
public static String[] buildCompilerArguments( CompilerConfiguration config,
String[] sourceFiles )
{
List args = new ArrayList();
// ----------------------------------------------------------------------
// Set output
// ----------------------------------------------------------------------
File destinationDir = new File( config.getOutputLocation() );
args.add( "-d" );
args.add( destinationDir.getAbsolutePath() );
// ----------------------------------------------------------------------
// Set the class and source paths
// ----------------------------------------------------------------------
List classpathEntries = config.getClasspathEntries();
if ( classpathEntries != null && !classpathEntries.isEmpty() )
{
args.add( "-classpath" );
args.add( getPathString( classpathEntries ) );
}
List sourceLocations = config.getSourceLocations();
if ( sourceLocations != null && !sourceLocations.isEmpty() )
{
//always pass source path, even if sourceFiles are declared,
//needed for jsr269 annotation processing, see MCOMPILER-98
args.add( "-sourcepath" );
args.add( getPathString( sourceLocations ) );
}
args.addAll(Arrays.asList(sourceFiles));
if ( !isPreJava16(config) )
{
//now add jdk 1.6 annotation processing related parameters
if ( config.getGeneratedSourcesDirectory() != null )
{
config.getGeneratedSourcesDirectory().mkdirs();
args.add( "-s" );
args.add( config.getGeneratedSourcesDirectory().getAbsolutePath() );
}
if ( config.getProc() != null )
{
args.add( "-proc:" + config.getProc() );
}
if ( config.getAnnotationProcessors() != null )
{
args.add("-processor");
String[] procs = config.getAnnotationProcessors();
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < procs.length; i++)
{
if (i > 0)
{
buffer.append( "," );
}
buffer.append( procs[i] );
}
args.add( buffer.toString() );
}
}
if ( config.isOptimize() )
{
args.add( "-O" );
}
if ( config.isDebug() )
{
if ( StringUtils.isNotEmpty( config.getDebugLevel() ) )
{
args.add( "-g:" + config.getDebugLevel() );
}
else
{
args.add( "-g" );
}
}
if ( config.isVerbose() )
{
args.add( "-verbose" );
}
if ( config.isShowDeprecation() )
{
args.add( "-deprecation" );
// This is required to actually display the deprecation messages
config.setShowWarnings( true );
}
if ( !config.isShowWarnings() )
{
args.add( "-nowarn" );
}
// TODO: this could be much improved
if ( StringUtils.isEmpty( config.getTargetVersion() ) )
{
// Required, or it defaults to the target of your JDK (eg 1.5)
args.add( "-target" );
args.add( "1.1" );
}
else
{
args.add( "-target" );
args.add( config.getTargetVersion() );
}
if ( !suppressSource( config ) && StringUtils.isEmpty( config.getSourceVersion() ) )
{
// If omitted, later JDKs complain about a 1.1 target
args.add( "-source" );
args.add( "1.3" );
}
else if ( !suppressSource( config ) )
{
args.add( "-source" );
args.add( config.getSourceVersion() );
}
if ( !suppressEncoding( config ) && !StringUtils.isEmpty( config.getSourceEncoding() ) )
{
args.add( "-encoding" );
args.add( config.getSourceEncoding() );
}
for ( Iterator it = config.getCustomCompilerArguments().entrySet().iterator(); it.hasNext(); )
{
Map.Entry entry = (Map.Entry) it.next();
String key = (String) entry.getKey();
if ( StringUtils.isEmpty( key ) )
{
continue;
}
args.add( key );
String value = (String) entry.getValue();
if ( StringUtils.isEmpty( value ) )
{
continue;
}
args.add( value );
}
return (String[]) args.toArray( new String[ args.size() ] );
}
/**
* Determine if the compiler is a version prior to 1.4.
* This is needed as 1.3 and earlier did not support -source or -encoding parameters
*
* @param config The compiler configuration to test.
* @return true if the compiler configuration represents a Java 1.4 compiler or later, false otherwise
*/
private static boolean isPreJava14( CompilerConfiguration config )
{
String v = config.getCompilerVersion();
if ( v == null )
{
return false;
}
return v.startsWith( "1.3" ) || v.startsWith( "1.2" )
|| v.startsWith( "1.1" ) || v.startsWith( "1.0" );
}
/**
* Determine if the compiler is a version prior to 1.6.
* This is needed for annotation processing parameters.
*
* @param config The compiler configuration to test.
* @return true if the compiler configuration represents a Java 1.6 compiler or later, false otherwise
*/
private static boolean isPreJava16( CompilerConfiguration config )
{
String v = config.getCompilerVersion();
if ( v == null )
{
//mkleint: i haven't completely understood the reason for the
//compiler version parameter, checking source as well, as most projects will have this one set, not the compiler
String s = config.getSourceVersion();
if ( s == null )
{
//now return true, as the 1.6 version is not the default - 1.4 is.
return true;
}
return s.startsWith( "1.5" ) || s.startsWith( "1.4" )
|| s.startsWith( "1.3" ) || s.startsWith( "1.2" )
|| s.startsWith( "1.1" ) || s.startsWith( "1.0" );
}
return v.startsWith( "1.5" ) || v.startsWith( "1.4" )
|| v.startsWith( "1.3" ) || v.startsWith( "1.2" )
|| v.startsWith( "1.1" ) || v.startsWith( "1.0" );
}
private static boolean suppressSource( CompilerConfiguration config )
{
return isPreJava14( config );
}
private static boolean suppressEncoding( CompilerConfiguration config )
{
return isPreJava14( config );
}
/**
* Compile the java sources in a external process, calling an external executable,
* like javac.
*
* @param config compiler configuration
* @param executable name of the executable to launch
* @param args arguments for the executable launched
* @return List of CompilerError objects with the errors encountered.
* @throws CompilerException
*/
List compileOutOfProcess( CompilerConfiguration config, String executable, String[] args )
throws CompilerException
{
Commandline cli = new Commandline();
cli.setWorkingDirectory( config.getWorkingDirectory().getAbsolutePath() );
cli.setExecutable( executable );
try
{
File argumentsFile = createFileWithArguments( args, config.getOutputLocation() );
cli.addArguments( new String[] { "@" + argumentsFile.getCanonicalPath().replace( File.separatorChar, '/' ) } );
if ( !StringUtils.isEmpty( config.getMaxmem() ) )
{
cli.addArguments( new String[] { "-J-Xmx" + config.getMaxmem() } );
}
if ( !StringUtils.isEmpty( config.getMeminitial() ) )
{
cli.addArguments( new String[] { "-J-Xms" + config.getMeminitial() } );
}
}
catch ( IOException e )
{
throw new CompilerException( "Error creating file with javac arguments", e );
}
CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
int returnCode;
List messages;
if ( ( getLogger() != null ) && getLogger().isDebugEnabled() )
{
File commandLineFile =
new File( config.getOutputLocation(), "javac." + ( Os.isFamily( Os.FAMILY_WINDOWS ) ? "bat" : "sh" ) );
try
{
FileUtils.fileWrite( commandLineFile.getAbsolutePath(),
cli.toString().replaceAll( "'", "" ) );
if ( !Os.isFamily( Os.FAMILY_WINDOWS ) )
{
Runtime.getRuntime().exec( new String[] { "chmod", "a+x", commandLineFile.getAbsolutePath() } );
}
}
catch ( IOException e )
{
if ( ( getLogger() != null ) && getLogger().isWarnEnabled() )
{
getLogger().warn( "Unable to write '" + commandLineFile.getName() + "' debug script file", e );
}
}
}
try
{
returnCode = CommandLineUtils.executeCommandLine( cli, out, err );
messages = parseModernStream( returnCode, new BufferedReader( new StringReader( err.getOutput() ) ) );
}
catch ( CommandLineException e )
{
throw new CompilerException( "Error while executing the external compiler.", e );
}
catch ( IOException e )
{
throw new CompilerException( "Error while executing the external compiler.", e );
}
if ( ( returnCode != 0 ) && messages.isEmpty() )
{
if ( err.getOutput().length() == 0 )
{
throw new CompilerException( "Unknown error trying to execute the external compiler: " + EOL
+ cli.toString() );
}
else
{
messages.add( new CompilerError( "Failure executing javac, but could not parse the error:" + EOL
+ err.getOutput(), true ) );
}
}
return messages;
}
/**
* Compile the java sources in the current JVM, without calling an external executable,
* using <code>com.sun.tools.javac.Main</code> class
*
* @param args arguments for the compiler as they would be used in the command line javac
* @return List of CompilerError objects with the errors encountered.
* @throws CompilerException
*/
List compileInProcess( String[] args )
throws CompilerException
{
IsolatedClassLoader cl = new IsolatedClassLoader();
File toolsJar = new File( System.getProperty( "java.home" ), "../lib/tools.jar" );
if ( toolsJar.exists() )
{
try
{
cl.addURL( toolsJar.toURI().toURL() );
}
catch ( MalformedURLException e )
{
throw new CompilerException( "Could not convert the file reference to tools.jar to a URL, path to tools.jar: '" + toolsJar.getAbsolutePath() + "'." );
}
}
Class c;
try
{
c = cl.loadClass( "com.sun.tools.javac.Main" );
}
catch ( ClassNotFoundException e )
{
String message = "Unable to locate the Javac Compiler in:" + EOL + " " + toolsJar + EOL
+ "Please ensure you are using JDK 1.4 or above and" + EOL
+ "not a JRE (the com.sun.tools.javac.Main class is required)." + EOL
+ "In most cases you can change the location of your Java" + EOL
+ "installation by setting the JAVA_HOME environment variable.";
return Collections.singletonList( new CompilerError( message, true ) );
}
StringWriter out = new StringWriter();
Integer ok;
List messages;
try
{
Method compile = c.getMethod( "compile", new Class[] { String[].class, PrintWriter.class } );
ok = (Integer) compile.invoke( null, new Object[] { args, new PrintWriter( out ) } );
messages = parseModernStream( ok.intValue(), new BufferedReader( new StringReader( out.toString() ) ) );
}
catch ( NoSuchMethodException e )
{
throw new CompilerException( "Error while executing the compiler.", e );
}
catch ( IllegalAccessException e )
{
throw new CompilerException( "Error while executing the compiler.", e );
}
catch ( InvocationTargetException e )
{
throw new CompilerException( "Error while executing the compiler.", e );
}
catch ( IOException e )
{
throw new CompilerException( "Error while executing the compiler.", e );
}
if ( ( ok.intValue() != 0 ) && messages.isEmpty() )
{
// TODO: exception?
messages.add( new CompilerError( "Failure executing javac, but could not parse the error:" + EOL +
out.toString(), true ) );
}
return messages;
}
/**
* Parse the output from the compiler into a list of CompilerError objects
*
* @param exitCode The exit code of javac.
* @param input The output of the compiler
* @return List of CompilerError objects
* @throws IOException
*/
static List parseModernStream( int exitCode, BufferedReader input )
throws IOException
{
List errors = new ArrayList();
String line;
StringBuffer buffer;
while ( true )
{
// cleanup the buffer
buffer = new StringBuffer(); // this is quicker than clearing it
// most errors terminate with the '^' char
do
{
line = input.readLine();
if ( line == null )
{
return errors;
}
// TODO: there should be a better way to parse these
if ( ( buffer.length() == 0 ) && line.startsWith( "error: " ) )
{
errors.add( new CompilerError( line, true ) );
}
else if ( ( buffer.length() == 0 ) && isNote( line ) )
{
// skip, JDK 1.5 telling us deprecated APIs are used but -Xlint:deprecation isn't set
}
else
{
buffer.append( line );
buffer.append( EOL );
}
}
while ( !line.endsWith( "^" ) );
// add the error bean
errors.add( parseModernError( exitCode, buffer.toString() ) );
}
}
private static boolean isNote( String line )
{
for ( int i = 0; i < NOTE_PREFIXES.length; i++ )
{
if ( line.startsWith( NOTE_PREFIXES[i] ) )
{
return true;
}
}
return false;
}
/**
* Construct a CompilerError object from a line of the compiler output
*
* @param exitCode The exit code from javac.
* @param error output line from the compiler
* @return the CompilerError object
*/
static CompilerError parseModernError( int exitCode, String error )
{
StringTokenizer tokens = new StringTokenizer( error, ":" );
boolean isError = exitCode != 0;
StringBuffer msgBuffer;
try
{
// With Java 6 error output lines from the compiler got longer. For backward compatibility
// .. and the time being, we eat up all (if any) tokens up to the erroneous file and source
// .. line indicator tokens.
boolean tokenIsAnInteger;
String previousToken = null;
String currentToken = null;
do
{
previousToken = currentToken;
currentToken = tokens.nextToken();
// Probably the only backward compatible means of checking if a string is an integer.
tokenIsAnInteger = true;
try
{
Integer.parseInt( currentToken );
}
catch ( NumberFormatException e )
{
tokenIsAnInteger = false;
}
}
while ( !tokenIsAnInteger );
String file = previousToken;
String lineIndicator = currentToken;
int startOfFileName = previousToken.lastIndexOf( "]" );
if ( startOfFileName > -1 )
{
file = file.substring( startOfFileName + 2 );
}
// When will this happen?
if ( file.length() == 1 )
{
file = new StringBuffer( file ).append( ":" ).append( tokens.nextToken() ).toString();
}
int line = Integer.parseInt( lineIndicator );
msgBuffer = new StringBuffer();
String msg = tokens.nextToken( EOL ).substring( 2 );
isError = exitCode != 0;
// Remove the 'warning: ' prefix
String warnPrefix = getWarnPrefix( msg );
if ( warnPrefix != null )
{
isError = false;
msg = msg.substring( warnPrefix.length() );
}
msgBuffer.append( msg );
msgBuffer.append( EOL );
String context = tokens.nextToken( EOL );
String pointer = tokens.nextToken( EOL );
if ( tokens.hasMoreTokens() )
{
msgBuffer.append( context ); // 'symbol' line
msgBuffer.append( EOL );
msgBuffer.append( pointer ); // 'location' line
msgBuffer.append( EOL );
context = tokens.nextToken( EOL );
try
{
pointer = tokens.nextToken( EOL );
}
catch ( NoSuchElementException e )
{
pointer = context;
context = null;
}
}
String message = msgBuffer.toString();
int startcolumn = pointer.indexOf( "^" );
int endcolumn = context == null ? startcolumn : context.indexOf( " ", startcolumn );
if ( endcolumn == -1 )
{
endcolumn = context.length();
}
return new CompilerError( file, isError, line, startcolumn, line, endcolumn, message.trim() );
}
catch ( NoSuchElementException e )
{
return new CompilerError( "no more tokens - could not parse error message: " + error, isError );
}
catch ( NumberFormatException e )
{
return new CompilerError( "could not parse error message: " + error, isError );
}
catch ( Exception e )
{
return new CompilerError( "could not parse error message: " + error, isError );
}
}
private static String getWarnPrefix( String msg )
{
for ( int i = 0; i < WARNING_PREFIXES.length; i++ )
{
if ( msg.startsWith( WARNING_PREFIXES[i] ) )
{
return WARNING_PREFIXES[i];
}
}
return null;
}
/**
* put args into a temp file to be referenced using the @ option in javac command line
* @param args
* @return the temporary file wth the arguments
* @throws IOException
*/
private File createFileWithArguments( String[] args, String outputDirectory )
throws IOException
{
PrintWriter writer = null;
try
{
File tempFile;
if ( ( getLogger() != null ) && getLogger().isDebugEnabled() )
{
tempFile = File.createTempFile( JavacCompiler.class.getName(), "arguments", new File( outputDirectory ) );
}
else
{
tempFile = File.createTempFile( JavacCompiler.class.getName(), "arguments" );
tempFile.deleteOnExit();
}
writer = new PrintWriter( new FileWriter( tempFile ) );
for ( int i = 0; i < args.length; i++ )
{
String argValue = args[i].replace( File.separatorChar, '/' );
writer.write( "\"" + argValue + "\"" );
writer.println();
}
writer.flush();
return tempFile;
}
finally
{
if ( writer != null )
{
writer.close();
}
}
}
/**
* Get the path of the javac tool executable: try to find it depending the OS or the <code>java.home</code>
* system property or the <code>JAVA_HOME</code> environment variable.
*
* @return the path of the Javadoc tool
* @throws IOException if not found
*/
private static String getJavacExecutable()
throws IOException
{
String javacCommand = "javac" + ( Os.isFamily( Os.FAMILY_WINDOWS ) ? ".exe" : "" );
String javaHome = System.getProperty( "java.home" );
File javacExe;
if ( Os.isName( "AIX" ) )
{
javacExe =
new File( javaHome + File.separator + ".." + File.separator + "sh",
javacCommand );
}
else if ( Os.isName( "Mac OS X" ) )
{
javacExe = new File( javaHome + File.separator + "bin", javacCommand );
}
else
{
javacExe =
new File( javaHome + File.separator + ".." + File.separator + "bin",
javacCommand );
}
// ----------------------------------------------------------------------
// Try to find javacExe from JAVA_HOME environment variable
// ----------------------------------------------------------------------
if ( !javacExe.isFile() )
{
Properties env = CommandLineUtils.getSystemEnvVars();
javaHome = env.getProperty( "JAVA_HOME" );
if ( StringUtils.isEmpty( javaHome ) )
{
throw new IOException( "The environment variable JAVA_HOME is not correctly set." );
}
if ( !new File( javaHome ).isDirectory() )
{
throw new IOException( "The environment variable JAVA_HOME=" + javaHome
+ " doesn't exist or is not a valid directory." );
}
javacExe = new File( env.getProperty( "JAVA_HOME" ) + File.separator + "bin", javacCommand );
}
if ( !javacExe.isFile() )
{
throw new IOException( "The javadoc executable '" + javacExe
+ "' doesn't exist or is not a file. Verify the JAVA_HOME environment variable." );
}
return javacExe.getAbsolutePath();
}
}