com.qwazr.compiler.JavaCompiler Maven / Gradle / Ivy
/**
* Copyright 2015-2017 Emmanuel Keller / QWAZR
*
* 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 com.qwazr.compiler;
import com.qwazr.classloader.ClassLoaderManager;
import com.qwazr.server.ServerException;
import com.qwazr.utils.LockUtils;
import com.qwazr.utils.StringUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.FileFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class JavaCompiler {
private final static Logger LOGGER = LoggerFactory.getLogger(JavaCompiler.class);
private final ClassLoaderManager classLoaderManager;
private final int javaSourcePrefixSize;
private final File javaSourceDirectory;
private final File javaClassesDirectory;
private final LinkedHashSet classPathSet;
private final String compiledClassPath;
private final LockUtils.ReadWriteLock compilerLock;
private final ConcurrentHashMap compilableMap;
private final ConcurrentHashMap diagnosticMap;
private JavaCompiler(final ClassLoaderManager classLoaderManager, final File javaSourceDirectory,
final File javaClassesDirectory, final LinkedHashSet classPathSet) throws IOException {
this.classLoaderManager = classLoaderManager;
this.classPathSet = classPathSet;
this.compiledClassPath = classPathSet != null && !classPathSet.isEmpty() ?
StringUtils.join(classPathSet, File.pathSeparatorChar) :
null;
this.compilableMap = new ConcurrentHashMap<>();
this.diagnosticMap = new ConcurrentHashMap<>();
this.javaSourceDirectory = javaSourceDirectory;
String javaSourcePrefix = javaSourceDirectory.getAbsolutePath();
javaSourcePrefixSize =
javaSourcePrefix.endsWith("/") ? javaSourcePrefix.length() : javaSourcePrefix.length() + 1;
this.javaClassesDirectory = javaClassesDirectory;
if (this.javaClassesDirectory != null && !this.javaClassesDirectory.exists())
this.javaClassesDirectory.mkdir();
compilerLock = new LockUtils.ReadWriteLock();
if (javaSourceDirectory.exists() && javaSourceDirectory.isDirectory())
compileDirectory(javaSourceDirectory);
else
LOGGER.warn("No java source directory: " + javaSourceDirectory.getAbsolutePath());
}
static JavaCompiler newInstance(final ClassLoaderManager classLoaderManager, final File javaSourceDirectory,
final File javaClassesDirectory, final File... classPathDirectories)
throws IOException, URISyntaxException {
Objects.requireNonNull(javaSourceDirectory, "No source directory given (null)");
Objects.requireNonNull(javaClassesDirectory, "No class directory given (null)");
final LinkedHashSet urlList = new LinkedHashSet<>();
urlList.add(javaClassesDirectory.toURI().toURL());
final LinkedHashSet classPath = buildClassPath(classPathDirectories, urlList);
return new JavaCompiler(classLoaderManager, javaSourceDirectory, javaClassesDirectory, classPath);
}
private static LinkedHashSet buildClassPath(final File[] classPathArray,
final Collection urlCollection) throws MalformedURLException, URISyntaxException {
final LinkedHashSet classPathes = new LinkedHashSet<>();
final String classPath = System.getProperty("java.class.path");
if (!StringUtils.isEmpty(classPath))
classPathes.addAll(Arrays.asList(StringUtils.split(classPath, File.pathSeparatorChar)));
if (classPathArray != null) {
for (File classPathFile : classPathArray) {
if (classPathFile.isDirectory()) {
File[] classPathFiles = classPathFile.listFiles((FileFilter) FileFileFilter.FILE);
if (classPathFiles != null) {
for (File f : classPathFiles) {
classPathes.add(f.getAbsolutePath());
urlCollection.add(f.toURI().toURL());
}
}
} else if (classPathFile.isFile()) {
classPathes.add(classPathFile.getAbsolutePath());
urlCollection.add(classPathFile.toURI().toURL());
}
}
}
return classPathes;
}
private boolean compile(final javax.tools.JavaCompiler compiler, final Collection javaFiles,
final AtomicInteger counter) throws IOException {
final DiagnosticCollector diagnostics = new DiagnosticCollector<>();
try (final StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) {
final Iterable extends JavaFileObject> sourceFileObjects =
fileManager.getJavaFileObjectsFromFiles(javaFiles);
final List options = new ArrayList<>();
if (compiledClassPath != null && !compiledClassPath.isEmpty()) {
options.add("-classpath");
options.add(compiledClassPath);
}
options.add("-d");
options.add(javaClassesDirectory.getAbsolutePath());
options.add("-sourcepath");
options.add(javaSourceDirectory.getAbsolutePath());
javax.tools.JavaCompiler.CompilationTask task =
compiler.getTask(null, fileManager, diagnostics, options, null, sourceFileObjects);
final Date date = new Date();
final boolean compilationSuccess = task.call();
for (File file : javaFiles) {
final URI fileUri = file.toURI();
compilableMap.put(fileUri, date);
diagnosticMap.remove(fileUri);
}
counter.addAndGet(javaFiles.size());
for (final Diagnostic extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
if (diagnostic == null)
continue;
final JavaFileObject source = diagnostic.getSource();
if (source == null)
continue;
final URI uri = source.toUri();
if (uri != null)
diagnosticMap.put(uri, new CompilerStatus.DiagnosticStatus(date, diagnostic));
if (LOGGER.isWarnEnabled())
LOGGER.warn(
String.format("Error on line %d in %s%n%s", diagnostic.getLineNumber(), source.getName(),
diagnostic.getMessage(null)));
}
return compilationSuccess;
}
}
private Collection filterUptodate(final File parentDir, final File[] javaSourceFiles) {
if (javaSourceFiles == null)
return null;
final Collection finalJavaFiles = new ArrayList<>();
if (javaSourceFiles.length == 0)
return finalJavaFiles;
final File parentClassDir =
new File(javaClassesDirectory, parentDir.getAbsolutePath().substring(javaSourcePrefixSize));
for (File javaSourceFile : javaSourceFiles) {
final File classFile =
new File(parentClassDir, FilenameUtils.removeExtension(javaSourceFile.getName()) + ".class");
if (classFile.exists() && classFile.lastModified() > javaSourceFile.lastModified()) {
compilableMap.put(javaSourceFile.toURI(), new Date(classFile.lastModified()));
continue;
}
finalJavaFiles.add(javaSourceFile);
}
return finalJavaFiles;
}
private void compileDirectory(final javax.tools.JavaCompiler compiler, final File sourceDirectory,
final AtomicInteger counter) {
final Collection javaFiles = filterUptodate(sourceDirectory, sourceDirectory.listFiles(javaFileFilter));
if (javaFiles != null && javaFiles.size() > 0) {
if (LOGGER.isInfoEnabled())
LOGGER.info("Compile " + javaFiles.size() + " JAVA file(s) at " + sourceDirectory);
try {
compile(compiler, javaFiles, counter);
} catch (IOException e) {
throw new ServerException(e);
}
}
final File[] dirFiles = sourceDirectory.listFiles((FileFilter) DirectoryFileFilter.INSTANCE);
if (dirFiles != null)
for (File dir : dirFiles)
compileDirectory(compiler, dir, counter);
}
private void compileDirectory(File sourceDirectory) throws IOException {
if (sourceDirectory == null)
return;
if (!sourceDirectory.isDirectory())
return;
compilerLock.writeEx(() -> {
javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
Objects.requireNonNull(compiler, "No compiler is available. This feature requires a JDK (not a JRE).");
final AtomicInteger counter = new AtomicInteger();
compileDirectory(compiler, sourceDirectory, counter);
if (counter.get() > 0)
classLoaderManager.reload();
});
}
private final JavaFileFilter javaFileFilter = new JavaFileFilter();
private class JavaFileFilter implements FileFilter {
@Override
final public boolean accept(File file) {
if (!file.isFile())
return false;
return file.getName().endsWith(".java");
}
}
CompilerStatus getStatus() {
return new CompilerStatus(compilableMap, diagnosticMap, classPathSet);
}
}