package net.rkunze.maven.compiler.jsr308javac; /** * The MIT License * * Copyright (c) 2005, The Codehaus * Copyright (c) 2014, Richard Kunze * * 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.io.BufferedReader; import java.io.File; 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.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.StringTokenizer; import java.util.concurrent.CopyOnWriteArrayList; import org.codehaus.plexus.compiler.AbstractCompiler; import org.codehaus.plexus.compiler.CompilerConfiguration; import org.codehaus.plexus.compiler.CompilerException; import org.codehaus.plexus.compiler.CompilerMessage; import org.codehaus.plexus.compiler.CompilerOutputStyle; import org.codehaus.plexus.compiler.CompilerResult; import org.codehaus.plexus.util.StringUtils; /** * @author <a href="mailto:trygvis@inamo.no">Trygve Laugstøl</a> * @author <a href="mailto:matthew.pocock@ncl.ac.uk">Matthew Pocock</a> * @author <a href="mailto:joerg.wassmer@web.de">Jörg Waßmer</a> * @author Others * @plexus.component role="org.codehaus.plexus.compiler.Compiler" * role-hint="javac+jsr308" */ public class JavacJSR308Compiler 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 " }; private static final Object LOCK = new Object(); private static final String JAVAC_CLASSNAME = "com.sun.tools.javac.Main"; private static volatile Class<?> JAVAC_CLASS; private List<Class<?>> javaccClasses = new CopyOnWriteArrayList<Class<?>>(); // ---------------------------------------------------------------------- // // ---------------------------------------------------------------------- public JavacJSR308Compiler() { super( CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE, ".java", ".class", null ); } // ---------------------------------------------------------------------- // Compiler Implementation // ---------------------------------------------------------------------- public CompilerResult performCompile( CompilerConfiguration config ) throws CompilerException { if ( config.isFork() ) { throw new CompilerException("'fork' not supported."); } File destinationDir = new File( config.getOutputLocation() ); if ( !destinationDir.exists() ) { destinationDir.mkdirs(); } String[] sourceFiles = getSourceFiles( config ); if ( ( sourceFiles == null ) || ( sourceFiles.length == 0 ) ) { return new CompilerResult(); } if ( ( getLogger() != null ) && getLogger().isInfoEnabled() ) { getLogger().info( "Compiling " + sourceFiles.length + " " + "source file" + ( sourceFiles.length == 1 ? "" : "s" ) + " to " + destinationDir.getAbsolutePath() ); } String[] args = buildCompilerArguments( config, sourceFiles ); CompilerResult result; result = compileInProcess( args, config ); return result; } public String[] createCommandLine( CompilerConfiguration config ) throws CompilerException { return buildCompilerArguments( config, getSourceFiles( config ) ); } public static String[] buildCompilerArguments( CompilerConfiguration config, String[] sourceFiles ) throws CompilerException { if (!ClasspathConfig.getCheckerJar().exists()) { throw new CompilerException("Type checker jar not found: " + ClasspathConfig.getCheckerJar().getAbsolutePath()); } if (!ClasspathConfig.getAnnotatedJDK(System.getProperty("java.version")).exists()) { throw new CompilerException("Annotaded JDK jar not found: " + ClasspathConfig.getAnnotatedJDK(System.getProperty("java.version")).getAbsolutePath()); } List<String> args = new ArrayList<String>(); // ---------------------------------------------------------------------- // Set output // ---------------------------------------------------------------------- File destinationDir = new File( config.getOutputLocation() ); args.add( "-d" ); args.add( destinationDir.getAbsolutePath() ); // ---------------------------------------------------------------------- // Set the class and source paths // ---------------------------------------------------------------------- // FIXME: Handle "bootclasspath" argument args.add( "-Xbootclasspath/p:" + getPathString(Arrays.asList( ClasspathConfig.getAnnotatedJDK(System.getProperty("java.version")).getAbsolutePath(), ClasspathConfig.getCompilerJar().getAbsolutePath() ))); args.add( "-classpath" ); List<String> originalClasspath = config.getClasspathEntries(); List<String> classpathEntries = new ArrayList<>(originalClasspath==null ? 1 : originalClasspath.size() + 1); classpathEntries.add(ClasspathConfig.getCheckerJar().getAbsolutePath()); if ( originalClasspath != null && !originalClasspath.isEmpty() ) { classpathEntries.addAll(originalClasspath); } args.add( getPathString( classpathEntries ) ); List<String> 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(); StringBuilder buffer = new StringBuilder(); 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 ( Map.Entry<String, String> entry : config.getCustomCompilerArgumentsAsMap().entrySet() ) { String key = entry.getKey(); if ( StringUtils.isEmpty( key ) || key.startsWith( "-J" ) ) { continue; } args.add( key ); String value = entry.getValue(); if ( StringUtils.isEmpty( value ) ) { continue; } args.add( value ); } return 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 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 * @param config compiler configuration * @return a CompilerResult object encapsulating the result of the compilation and any compiler messages * @throws CompilerException */ CompilerResult compileInProcess( String[] args, CompilerConfiguration config ) throws CompilerException { final Class<?> javacClass = getJavacClass( config ); final Thread thread = Thread.currentThread(); final ClassLoader contextClassLoader = thread.getContextClassLoader(); thread.setContextClassLoader( javacClass.getClassLoader() ); getLogger().debug( "ttcl changed run compileInProcessWithProperClassloader" ); try { return compileInProcessWithProperClassloader(javacClass, args); } finally { releaseJavaccClass( javacClass, config ); thread.setContextClassLoader( contextClassLoader ); } } protected CompilerResult compileInProcessWithProperClassloader( Class<?> javacClass, String[] args ) throws CompilerException { return compileInProcess0(javacClass, args); } /** * Helper method for compileInProcess() */ private static CompilerResult compileInProcess0( Class<?> javacClass, String[] args ) throws CompilerException { StringWriter out = new StringWriter(); Integer ok; List<CompilerMessage> messages; try { Method compile = javacClass.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 ); } boolean success = ok.intValue() == 0; return new CompilerResult( success, messages ); } /** * Parse the output from the compiler into a list of CompilerMessage objects * * @param exitCode The exit code of javac. * @param input The output of the compiler * @return List of CompilerMessage objects * @throws IOException */ static List<CompilerMessage> parseModernStream( int exitCode, BufferedReader input ) throws IOException { List<CompilerMessage> errors = new ArrayList<CompilerMessage>(); String line; StringBuilder buffer; while ( true ) { // cleanup the buffer buffer = new StringBuilder(); // this is quicker than clearing it // most errors terminate with the '^' char do { line = input.readLine(); if ( line == null ) { // javac output not detected by other parsing if ( buffer.length() > 0 && buffer.toString().startsWith( "javac:" ) ) { errors.add( new CompilerMessage( buffer.toString(), CompilerMessage.Kind.ERROR ) ); } return errors; } // TODO: there should be a better way to parse these if ( ( buffer.length() == 0 ) && line.startsWith( "error: " ) ) { errors.add( new CompilerMessage( 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 CompilerMessage object from a line of the compiler output * * @param exitCode The exit code from javac. * @param error output line from the compiler * @return the CompilerMessage object */ static CompilerMessage parseModernError( int exitCode, String error ) { StringTokenizer tokens = new StringTokenizer( error, ":" ); boolean isError = exitCode != 0; StringBuilder 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 file = null; String currentToken = null; do { if ( currentToken != null ) { if ( file == null ) { file = currentToken; } else { file = file + ':' + 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 lineIndicator = currentToken; int startOfFileName = file.lastIndexOf( ']' ); if ( startOfFileName > -1 ) { file = file.substring( startOfFileName + 1 + EOL.length() ); } int line = Integer.parseInt( lineIndicator ); msgBuffer = new StringBuilder(); String msg = tokens.nextToken( EOL ).substring( 2 ); // Remove the 'warning: ' prefix String warnPrefix = getWarnPrefix( msg ); if ( warnPrefix != null ) { isError = false; msg = msg.substring( warnPrefix.length() ); } else { isError = exitCode != 0; } 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 CompilerMessage( file, isError, line, startcolumn, line, endcolumn, message.trim() ); } catch ( NoSuchElementException e ) { return new CompilerMessage( "no more tokens - could not parse error message: " + error, isError ); } catch ( NumberFormatException e ) { return new CompilerMessage( "could not parse error message: " + error, isError ); } catch ( Exception e ) { return new CompilerMessage( "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; } private void releaseJavaccClass( Class<?> javaccClass, CompilerConfiguration compilerConfiguration ) { if ( compilerConfiguration.getCompilerReuseStrategy() == CompilerConfiguration.CompilerReuseStrategy.ReuseCreated ) { javaccClasses.add( javaccClass ); } } /** * Find the main class of JavaC. Return the same class for subsequent calls. * * @return the non-null class. * @throws CompilerException if the class has not been found. */ private Class<?> getJavacClass( CompilerConfiguration compilerConfiguration ) throws CompilerException { Class<?> c = null; switch ( compilerConfiguration.getCompilerReuseStrategy() ) { case AlwaysNew: return createJavacClass(); case ReuseCreated: synchronized ( javaccClasses ) { if ( javaccClasses.size() > 0 ) { c = javaccClasses.get( 0 ); javaccClasses.remove( c ); return c; } } c = createJavacClass(); return c; case ReuseSame: default: c = JavacJSR308Compiler.JAVAC_CLASS; if ( c != null ) { return c; } synchronized ( JavacJSR308Compiler.LOCK ) { if ( c == null ) { JavacJSR308Compiler.JAVAC_CLASS = c = createJavacClass(); } return c; } } } /** * Helper method for create Javac class */ protected Class<?> createJavacClass() throws CompilerException { try { if (!ClasspathConfig.getCompilerJar().exists()) { throw new CompilerException("Javac jar file not found: " + ClasspathConfig.getCompilerJar().getAbsolutePath()); } ClassLoader javacClassLoader = new DelegateLastClassLoader( ((URLClassLoader)getClass().getClassLoader()).getURLs() ); final Thread thread = Thread.currentThread(); final ClassLoader contextClassLoader = thread.getContextClassLoader(); thread.setContextClassLoader( javacClassLoader ); try { return javacClassLoader.loadClass( JavacJSR308Compiler.JAVAC_CLASSNAME ); } finally { thread.setContextClassLoader( contextClassLoader ); } } catch ( ClassNotFoundException ex ) { throw new CompilerException( "Unable to locate the javac compiler in " + ClasspathConfig.getCompilerJar().getAbsolutePath(), ex ); } } }