/**
* Copyright 2011 Adrian Witas
*
* 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.
*/
package org.abstractmeta.toolbox.compilation.compiler.impl;
import com.google.common.io.Files;
import org.abstractmeta.toolbox.compilation.compiler.JavaSourceCompiler;
import org.abstractmeta.toolbox.compilation.compiler.registry.JavaFileObjectRegistry;
import org.abstractmeta.toolbox.compilation.compiler.registry.impl.JavaFileObjectRegistryImpl;
import org.abstractmeta.toolbox.compilation.compiler.util.ClassPathUtil;
import org.abstractmeta.toolbox.compilation.compiler.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.tools.*;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.*;
/**
* Provides implementation of JavaSourceCompiler interface.
* This implementation uses {@link javax.tools.JavaCompiler}.
* <p><b>Usage:</b>
* <code><pre>
* JavaSourceCompiler javaSourceCompiler = new JavaSourceCompilerImpl();
* JavaSourceCompiler.CompilationUnit compilationUnit = javaSourceCompiler.createCompilationUnit();
* compilationUnit.addJavaSource("com.test.foo.Foo","package com.test.foo;" +
* "public class Foo {\n" +
* " public static void main(String [] args) {\n" +
* " System.out.println(\"Hello world\");\n" +
* " }\n" +
* "}");
* ClassLoader classLoader = javaSourceCompiler.compile(compilationUnit);
* Class clazz = classLoader.loadClass("com.test.foo.Foo");
* Object foo = clazz.newInstance();
* Method main = clazz.getMethod("main", String[].class);
* String args = null;
* main.invoke(foo, args);
* </pre></code>
* </p>
* <p/>
* <p/>
* <p><i>Note</i> that to be able to use java compiler you will have to add tools.jar to your class path.
* </p>
*
* @author Adrian Witas
*/
public class JavaSourceCompilerImpl implements JavaSourceCompiler {
private final Logger logger = LoggerFactory.getLogger(JavaSourceCompilerImpl.class.getName());
private static final List<String> CLASS_PATH_OPTIONS = new ArrayList<String>(Arrays.asList("cp", "classpath"));
private static final String CLASS_PATH_DELIMITER = ClassPathUtil.getClassPathSeparator();
@Override
public ClassLoader compile(CompilationUnit compilationUnit, String... options) {
return compile(this.getClass().getClassLoader(), compilationUnit, options);
}
@Override
public ClassLoader compile(ClassLoader parentClassLoader, CompilationUnit compilationUnit, String... options) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
return compile(compiler, parentClassLoader, compilationUnit, options);
}
protected ClassLoader compile(JavaCompiler compiler, ClassLoader parentClassLoader, CompilationUnit compilationUnit, String... options) {
if (compiler == null) {
throw new IllegalStateException("Failed to find the system Java compiler. Check that your class path includes tools.jar");
}
JavaFileObjectRegistry registry = compilationUnit.getRegistry();
SimpleClassLoader result = new SimpleClassLoader(parentClassLoader, registry, compilationUnit.getOutputClassDirectory());
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
JavaFileManager standardFileManager = compiler.getStandardFileManager(diagnostics, null, null);
JavaFileManager javaFileManager = new SimpleJavaFileManager(standardFileManager, result, registry);
Iterable<JavaFileObject> sources = registry.get(JavaFileObject.Kind.SOURCE);
Collection<String> compilationOptions = buildOptions(compilationUnit, result, options);
JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnostics, compilationOptions, null, sources);
task.call();
if (getDiagnosticCountByType(diagnostics, Diagnostic.Kind.ERROR) > 0) {
throw createCompilationErrorException(registry, diagnostics);
}
if (getDiagnosticCountByType(diagnostics, Diagnostic.Kind.WARNING) > 0) {
logger.warn(getDiagnosticString(registry, diagnostics));
}
result.addClassPathEntries(compilationUnit.getClassPathsEntries());
return result;
}
protected boolean buildDiagnosticMessage(Diagnostic diagnostic, StringBuilder diagnosticBuilder, JavaFileObjectRegistry registry) {
Object source = diagnostic.getSource();
String sourceErrorDetails = "";
if (source != null) {
JavaSourceFileObject sourceFile = JavaSourceFileObject.class.cast(source);
CharSequence sourceCode = sourceFile.getCharContent(true);
int startPosition = Math.max((int) diagnostic.getStartPosition() - 10, 0);
int endPosition = Math.min(sourceCode.length(), (int) diagnostic.getEndPosition() + 10);
sourceErrorDetails = sourceCode.subSequence(startPosition, endPosition) + "";
}
diagnosticBuilder.append(diagnostic.getMessage(null));
diagnosticBuilder.append("\n");
diagnosticBuilder.append(sourceErrorDetails);
return diagnostic.getKind().equals(Diagnostic.Kind.ERROR);
}
protected Collection<String> buildOptions(CompilationUnit compilationUnit, SimpleClassLoader classLoader, String... options) {
List<String> result = new ArrayList<String>();
Map<String, String> optionsMap = new HashMap<String, String>();
for (int i = 0; i < options.length; i += 2) {
optionsMap.put(options[i], options[i + 1]);
}
for (String classPathKey : CLASS_PATH_OPTIONS) {
if (optionsMap.containsKey(classPathKey)) {
addClassPath(compilationUnit, optionsMap.get(classPathKey));
}
}
for (String key : optionsMap.keySet()) {
if (CLASS_PATH_OPTIONS.contains(key)) {
continue;
}
result.addAll(Arrays.asList(key, optionsMap.get(key)));
}
addClassPath(result, compilationUnit);
return result;
}
/**
* Adds given class path entries of compilation unit to the supplied option result list.
* This method simply add -cp 'cass_path_entry1:...:clas_path_entry_x' options
*
* @param result result list
* @param compilationUnit compilation unit
*/
private void addClassPath(List<String> result, CompilationUnit compilationUnit) {
StringBuilder classPathBuilder = new StringBuilder();
for (String entry : compilationUnit.getClassPathsEntries()) {
if (classPathBuilder.length() > 0) {
classPathBuilder.append(CLASS_PATH_DELIMITER);
}
classPathBuilder.append(entry);
}
if (classPathBuilder.length() > 0) {
result.addAll(Arrays.asList("-cp", classPathBuilder.toString()));
}
}
protected void addClassPath(CompilationUnit result, String classPath) {
String[] classPathEntries = classPath.split(CLASS_PATH_DELIMITER);
for (String classPathEntry : classPathEntries) {
result.addClassPathEntry(classPathEntry);
}
}
@Override
public CompilationUnit createCompilationUnit() {
File outputDirectory = new File(System.getProperty("java.io.tmpdir"), "compiled-code_" + System.currentTimeMillis());
return createCompilationUnit(outputDirectory);
}
@Override
public CompilationUnit createCompilationUnit(File outputClassDirectory) {
return new CompilationUnitImpl(outputClassDirectory);
}
protected IllegalStateException createCompilationErrorException(JavaFileObjectRegistry registry, DiagnosticCollector<JavaFileObject> diagnostics) {
return new IllegalStateException(getDiagnosticString(registry, diagnostics));
}
private String getDiagnosticString(JavaFileObjectRegistry registry, DiagnosticCollector<JavaFileObject> diagnostics) {
StringBuilder diagnosticBuilder = new StringBuilder();
for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
buildDiagnosticMessage(diagnostic, diagnosticBuilder, registry);
}
return diagnosticBuilder.toString();
}
private long getDiagnosticCountByType(DiagnosticCollector<JavaFileObject> diagnostics, Diagnostic.Kind kind) {
long result = 0;
for (Diagnostic<? extends JavaFileObject> e : diagnostics.getDiagnostics()) {
if (e.getKind().equals(kind)) {
result += 1;
}
}
return result;
}
public void persistCompiledClasses(CompilationUnit compilationUnit) {
JavaFileObjectRegistry registry = compilationUnit.getRegistry();
File classOutputDirectory = compilationUnit.getOutputClassDirectory();
if (!classOutputDirectory.exists()) {
if(!classOutputDirectory.mkdirs()) throw new IllegalStateException("Failed to create directory " +classOutputDirectory.getAbsolutePath());
}
for (JavaFileObject javaFileObject : registry.get(JavaFileObject.Kind.CLASS)) {
String internalName = javaFileObject.getName().substring(1);
File compiledClassFile = new File(classOutputDirectory, internalName);
if (!compiledClassFile.getParentFile().exists()) {
if (!compiledClassFile.getParentFile().mkdirs()) {
throw new IllegalStateException("Failed to create directories " + compiledClassFile.getParent());
}
}
try {
Files.write(JavaCodeFileObject.class.cast(javaFileObject).getByteCode(), compiledClassFile);
} catch (IOException e) {
throw new IllegalStateException("Failed to write to file " + compiledClassFile, e);
}
}
}
public static class CompilationUnitImpl implements CompilationUnit {
private final List<String> classPathEntries = new ArrayList<String>();
private final JavaFileObjectRegistry registry = new JavaFileObjectRegistryImpl();
private final File outputClassDirectory;
public CompilationUnitImpl(File outputClassDirectory) {
this.outputClassDirectory = outputClassDirectory;
}
@Override
public void addClassPathEntry(String classPathEntry) {
classPathEntries.add(classPathEntry);
}
@Override
public void addClassPathEntries(Collection<String> classPathEntries) {
this.classPathEntries.addAll(classPathEntries);
}
@Override
public void addJavaSource(String className, String source) {
URI sourceUri = URIUtil.buildUri(StandardLocation.SOURCE_OUTPUT, className);
registry.register(new JavaSourceFileObject(sourceUri, source));
}
@Override
public JavaFileObjectRegistry getRegistry() {
return registry;
}
@Override
public List<String> getClassPathsEntries() {
return classPathEntries;
}
public File getOutputClassDirectory() {
return outputClassDirectory;
}
}
}