/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.tck.util; import static java.io.File.pathSeparator; import static java.util.stream.Collectors.toList; import static javax.tools.ToolProvider.getSystemJavaCompiler; import static org.apache.commons.io.FileUtils.listFiles; import static org.apache.commons.io.filefilter.TrueFileFilter.TRUE; import static org.mule.runtime.api.util.Preconditions.checkArgument; import static org.mule.tck.ZipUtils.compress; import org.mule.runtime.core.util.ClassUtils; import org.mule.runtime.core.util.StringUtils; import org.mule.tck.ZipUtils; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.StringJoiner; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import org.apache.commons.io.filefilter.NameFileFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Tools to compile Java files into classes, jars and Mule extensions. */ public class CompilerUtils { private static final Logger logger = LoggerFactory.getLogger(CompilerUtils.class); // Class used to compile extension annotations. This class must be in the classpath only when // a jar is compiled as an extension. private static final String EXTENSION_ANNOTATION_PROCESSOR_CLASSNAME = "org.mule.runtime.module.extension.internal.resources.ExtensionResourcesGeneratorAnnotationProcessor"; /** * Base class to create compiler utilities. * @param <T> class of the implemented compiler */ private static abstract class AbstractCompiler<T extends AbstractCompiler> { protected File[] requiredJars = {}; protected File[] sources = {}; /** * @return current instance. Used just to avoid compilation warnings. */ protected abstract T getThis(); /** * Adds jar files to the classpath used during the compilation. * * @param requiredJars jars to include in the classpath. Non null. * @return the same compiler instance */ public T dependingOn(File... requiredJars) { this.requiredJars = requiredJars; return getThis(); } /** * @return a folder to write the files resulting from the compilation */ protected File createTargetFolder() { try { File tempFolder = File.createTempFile(CompilerUtils.class.getSimpleName(), ""); tempFolder.delete(); tempFolder.mkdir(); return tempFolder; } catch (IOException e) { throw new RuntimeException(e); } } /** * Compiles all the Java sources defined on the compiler. * * @param targetFolder folder where the compilation result will be written. Non null. */ protected void compileJavaSources(File targetFolder) { checkArgument(targetFolder != null, "targetFolder cannot be null"); CompilerTask compilerTask = new CompilerTaskBuilder().compiling(sources) .dependingOn(requiredJars).toTarget(targetFolder).build(); compilerTask.compile(); } } /** * Compiles a single Java file into a Java class. */ public static class SingleClassCompiler extends AbstractCompiler<SingleClassCompiler> { private File targetFolder; /** * Compiles a single Java file. * * @param source file to compile. Non null. * @return the compiled class file */ public File compile(File source) { checkArgument(source != null, "source cannot be null"); targetFolder = createTargetFolder(); sources = new File[] {source}; compileJavaSources(targetFolder); return getCompiledClass(targetFolder, source.getName()); } /** * @return the folder where compiled classes where written or null if {@link #compile(File)} was not execute yet. */ public File getTargetFolder() { return targetFolder; } private File getCompiledClass(File targetFolder, String name) { String className = name.replace("java", "class"); Collection<File> classes = listFiles(targetFolder, new NameFileFilter(className), TRUE); if (classes.size() > 1) { throw new IllegalStateException("Cannot return compiled class as there are more than one compiled class file"); } return classes.iterator().next(); } @Override protected SingleClassCompiler getThis() { return this; } } /** * Base class to create a compiler that compiles multiple source files. * @param <T> class of the implemented compiler */ protected static abstract class MultipleFileCompiler<T extends MultipleFileCompiler> extends AbstractCompiler<T> { private List<ZipUtils.ZipResource> configuredResources = new ArrayList<>(); /** * Indicates which source file must be compiled. * * @param sources source files. Non empty. * @return the same compiler instance */ public T compiling(File... sources) { checkArgument(sources != null && sources.length > 0, "source cannot be empty"); this.sources = sources; return getThis(); } /** * Includes a resource file into the generated JAR file. * * @param resource resource file. Non empty. * @return the same compiler instance */ public T including(File resource, String alias) { configuredResources.add(new ZipUtils.ZipResource(resource.getAbsolutePath(), alias)); return getThis(); } /** * Generates a JAR file form the compiled files * * @param targetFolder folder containing the compiled files. Non null. * @param jarName name of the JAR file. Non empty. * @return */ protected File compressGeneratedFiles(File targetFolder, String jarName) { checkArgument(targetFolder != null, "targetFolder cannot be byll"); checkArgument(!StringUtils.isEmpty(jarName), "jar name cannot be empty"); Collection<File> files = listFiles(targetFolder, TRUE, TRUE); ZipUtils.ZipResource[] resources = getZipResources(targetFolder, files); File targetFile = new File(targetFolder, jarName); compress(targetFile, resources); return targetFile; } private ZipUtils.ZipResource[] getZipResources(File targetFolder, Collection<File> classes) { List<ZipUtils.ZipResource> compiledResources = classes.stream() .map(f -> new ZipUtils.ZipResource(f.getAbsolutePath(), getRelativePath(targetFolder, f))).collect(toList()); compiledResources.addAll(configuredResources); return compiledResources.toArray(new ZipUtils.ZipResource[0]); } private String getRelativePath(File targetFolder, File file) { final StringJoiner pathJoiner = new StringJoiner("/"); for (Path targetFolderPathElement : targetFolder.toPath().relativize(file.toPath())) { pathJoiner.add(targetFolderPathElement.toString()); } return pathJoiner.toString(); } } /** * Compiles a set of Java sources into a Jar file. */ public static class JarCompiler extends MultipleFileCompiler<JarCompiler> { /** * Compiles all the provided sources generating a JAR file. * * @param jarName name of the JAR file to create. Non empty. * @return the created file. */ public File compile(String jarName) { File targetFolder = createTargetFolder(); compileJavaSources(targetFolder); return compressGeneratedFiles(targetFolder, jarName); } @Override protected JarCompiler getThis() { return this; } } /** * Compiles a set of Java sources defining a Mule extension into a Jar file. */ public static class ExtensionCompiler extends MultipleFileCompiler<ExtensionCompiler> { @Override protected ExtensionCompiler getThis() { return this; } /** * Compiles all the provided sources generating a JAR file. * * @param jarName name of the JAR file to create. Non empty. * @param extensionVersion version of the extension being compiled. Non empty. * @return the created file. */ public File compile(String jarName, String extensionVersion) { checkArgument(!StringUtils.isEmpty(jarName), "jarName cannot be empty"); checkArgument(!StringUtils.isEmpty(extensionVersion), "extensionVersion cannot be empty"); File targetFolder = createTargetFolder(); compileJavaSources(targetFolder); processExtensionAnnotations(targetFolder, extensionVersion); return compressGeneratedFiles(targetFolder, jarName); } private void processExtensionAnnotations(File targetFolder, String extensionVersion) { URLClassLoader urlClassLoader = createExtensionClassLoader(targetFolder); ClassUtils.withContextClassLoader(urlClassLoader, () -> { File metaInfFolder = new File(targetFolder, "META-INF"); metaInfFolder.mkdir(); CompilerTask compilerTask = new CompilerTaskBuilder().compiling(sources).dependingOn(requiredJars) .withProperty("extension.version", extensionVersion) .processingAnnotations(EXTENSION_ANNOTATION_PROCESSOR_CLASSNAME) .toTarget(metaInfFolder).build(); compilerTask.compile(); }); } private URLClassLoader createExtensionClassLoader(File targetFolder) { URLClassLoader urlClassLoader; try { urlClassLoader = new URLClassLoader(new URL[] {targetFolder.toURL()}); } catch (MalformedURLException e) { throw new RuntimeException(e); } return urlClassLoader; } } private static class CompilerTaskBuilder { private File target; private File[] sources = {}; private File[] jarFiles = {}; private String annotationProcessorClassName; private final List<String> processProperties = new ArrayList<>(); public CompilerTaskBuilder toTarget(File target) { this.target = target; return this; } public CompilerTaskBuilder dependingOn(File... jarFiles) { this.jarFiles = jarFiles; return this; } public CompilerTaskBuilder compiling(File... sources) { this.sources = sources; return this; } public CompilerTaskBuilder withProperty(String name, String value) { processProperties.add("-A" + name + "=" + value); return this; } public CompilerTaskBuilder processingAnnotations(String annotationProcessorClassName) { this.annotationProcessorClassName = annotationProcessorClassName; return this; } public CompilerTask build() { if (sources.length == 0) { throw new IllegalArgumentException("Must define at least a source file to compile"); } return new CompilerTask(sources, getOptions()); } private List<String> getOptions() { List<String> options = new ArrayList<>(); if (logger.isInfoEnabled()) { options.add("-verbose"); } if (annotationProcessorClassName == null) { // Disables annotation processing to avoid warnings options.add("-proc:none"); } else { options.add("-processor"); options.add(annotationProcessorClassName); options.add("-proc:only"); } if (target != null) { options.add("-d"); options.add(target.getAbsolutePath()); } if (jarFiles.length > 0) { // Adds same classpath as the one used on the runner String classPath = System.getProperty("java.class.path"); // Adds extra jars files required to compile the source classes for (File jarFile : jarFiles) { classPath = classPath + pathSeparator + jarFile.getAbsolutePath(); } options.addAll(Arrays.asList("-classpath", classPath)); } options.addAll(processProperties); return options; } } private static class CompilerTask { private final File[] sources; private final List<String> options; private CompilerTask(File[] sources, List<String> options) { this.sources = sources; this.options = options; } public void compile() { final JavaCompiler compiler = getSystemJavaCompiler(); final StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); try { Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(sources); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, null, compilationUnits); Boolean status = task.call(); if (!status) { throw new RuntimeException("Compiler task finished with error. Enable logging to find more information"); } } catch (Throwable e) { logger.error("Error processing compilation task", e); throw e; } finally { try { fileManager.close(); } catch (IOException e) { // Ignore } } } } }