/*
* Copyright 2012-2017 the original author or authors.
*
* 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.springframework.boot.cli.compiler;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyClassLoader.ClassCollector;
import groovy.lang.GroovyCodeSource;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.ASTTransformationVisitor;
import org.springframework.boot.cli.compiler.dependencies.SpringBootDependenciesDependencyManagement;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngineFactory;
import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext;
import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller;
import org.springframework.boot.cli.util.ResourceUtils;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
/**
* Compiler for Groovy sources. Primarily a simple Facade for
* {@link GroovyClassLoader#parseClass(GroovyCodeSource)} with the following additional
* features:
* <ul>
* <li>{@link CompilerAutoConfiguration} strategies will be read from
* {@code META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration}
* (per the standard java {@link ServiceLoader} contract) and applied during compilation
* </li>
*
* <li>Multiple classes can be returned if the Groovy source defines more than one Class
* </li>
*
* <li>Generated class files can also be loaded using
* {@link ClassLoader#getResource(String)}</li>
* </ul>
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
*/
public class GroovyCompiler {
private final GroovyCompilerConfiguration configuration;
private final ExtendedGroovyClassLoader loader;
private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations;
private final List<ASTTransformation> transformations;
/**
* Create a new {@link GroovyCompiler} instance.
* @param configuration the compiler configuration
*/
public GroovyCompiler(final GroovyCompilerConfiguration configuration) {
this.configuration = configuration;
this.loader = createLoader(configuration);
DependencyResolutionContext resolutionContext = new DependencyResolutionContext();
resolutionContext.addDependencyManagement(
new SpringBootDependenciesDependencyManagement());
AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader,
configuration.getRepositoryConfiguration(), resolutionContext,
configuration.isQuiet());
GrapeEngineInstaller.install(grapeEngine);
this.loader.getConfiguration()
.addCompilationCustomizers(new CompilerAutoConfigureCustomizer());
if (configuration.isAutoconfigure()) {
this.compilerAutoConfigurations = ServiceLoader
.load(CompilerAutoConfiguration.class);
}
else {
this.compilerAutoConfigurations = Collections.emptySet();
}
this.transformations = new ArrayList<>();
this.transformations
.add(new DependencyManagementBomTransformation(resolutionContext));
this.transformations.add(new DependencyAutoConfigurationTransformation(
this.loader, resolutionContext, this.compilerAutoConfigurations));
this.transformations.add(new GroovyBeansTransformation());
if (this.configuration.isGuessDependencies()) {
this.transformations.add(
new ResolveDependencyCoordinatesTransformation(resolutionContext));
}
for (ASTTransformation transformation : ServiceLoader
.load(SpringBootAstTransformation.class)) {
this.transformations.add(transformation);
}
Collections.sort(this.transformations, AnnotationAwareOrderComparator.INSTANCE);
}
/**
* Return a mutable list of the {@link ASTTransformation}s to be applied during
* {@link #compile(String...)}.
* @return the AST transformations to apply
*/
public List<ASTTransformation> getAstTransformations() {
return this.transformations;
}
public ExtendedGroovyClassLoader getLoader() {
return this.loader;
}
private ExtendedGroovyClassLoader createLoader(
GroovyCompilerConfiguration configuration) {
ExtendedGroovyClassLoader loader = new ExtendedGroovyClassLoader(
configuration.getScope());
for (URL url : getExistingUrls()) {
loader.addURL(url);
}
for (String classpath : configuration.getClasspath()) {
loader.addClasspath(classpath);
}
return loader;
}
private URL[] getExistingUrls() {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl instanceof ExtendedGroovyClassLoader) {
return ((ExtendedGroovyClassLoader) tccl).getURLs();
}
else {
return new URL[0];
}
}
public void addCompilationCustomizers(CompilationCustomizer... customizers) {
this.loader.getConfiguration().addCompilationCustomizers(customizers);
}
/**
* Compile the specified Groovy sources, applying any
* {@link CompilerAutoConfiguration}s. All classes defined in the sources will be
* returned from this method.
* @param sources the sources to compile
* @return compiled classes
* @throws CompilationFailedException in case of compilation failures
* @throws IOException in case of I/O errors
* @throws CompilationFailedException in case of compilation errors
*/
public Class<?>[] compile(String... sources)
throws CompilationFailedException, IOException {
this.loader.clearCache();
List<Class<?>> classes = new ArrayList<>();
CompilerConfiguration configuration = this.loader.getConfiguration();
CompilationUnit compilationUnit = new CompilationUnit(configuration, null,
this.loader);
ClassCollector collector = this.loader.createCollector(compilationUnit, null);
compilationUnit.setClassgenCallback(collector);
for (String source : sources) {
List<String> paths = ResourceUtils.getUrls(source, this.loader);
for (String path : paths) {
compilationUnit.addSource(new URL(path));
}
}
addAstTransformations(compilationUnit);
compilationUnit.compile(Phases.CLASS_GENERATION);
for (Object loadedClass : collector.getLoadedClasses()) {
classes.add((Class<?>) loadedClass);
}
ClassNode mainClassNode = MainClass.get(compilationUnit);
Class<?> mainClass = null;
for (Class<?> loadedClass : classes) {
if (mainClassNode.getName().equals(loadedClass.getName())) {
mainClass = loadedClass;
}
}
if (mainClass != null) {
classes.remove(mainClass);
classes.add(0, mainClass);
}
return classes.toArray(new Class<?>[classes.size()]);
}
@SuppressWarnings("rawtypes")
private void addAstTransformations(CompilationUnit compilationUnit) {
LinkedList[] phaseOperations = getPhaseOperations(compilationUnit);
processConversionOperations(phaseOperations[Phases.CONVERSION]);
}
@SuppressWarnings("rawtypes")
private LinkedList[] getPhaseOperations(CompilationUnit compilationUnit) {
try {
Field field = CompilationUnit.class.getDeclaredField("phaseOperations");
field.setAccessible(true);
LinkedList[] phaseOperations = (LinkedList[]) field.get(compilationUnit);
return phaseOperations;
}
catch (Exception ex) {
throw new IllegalStateException(
"Phase operations not available from compilation unit");
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void processConversionOperations(LinkedList conversionOperations) {
int index = getIndexOfASTTransformationVisitor(conversionOperations);
conversionOperations.add(index, new CompilationUnit.SourceUnitOperation() {
@Override
public void call(SourceUnit source) throws CompilationFailedException {
ASTNode[] nodes = new ASTNode[] { source.getAST() };
for (ASTTransformation transformation : GroovyCompiler.this.transformations) {
transformation.visit(nodes, source);
}
}
});
}
private int getIndexOfASTTransformationVisitor(LinkedList<?> conversionOperations) {
for (int index = 0; index < conversionOperations.size(); index++) {
if (conversionOperations.get(index).getClass().getName()
.startsWith(ASTTransformationVisitor.class.getName())) {
return index;
}
}
return conversionOperations.size();
}
/**
* {@link CompilationCustomizer} to call {@link CompilerAutoConfiguration}s.
*/
private class CompilerAutoConfigureCustomizer extends CompilationCustomizer {
CompilerAutoConfigureCustomizer() {
super(CompilePhase.CONVERSION);
}
@Override
public void call(SourceUnit source, GeneratorContext context, ClassNode classNode)
throws CompilationFailedException {
ImportCustomizer importCustomizer = new SmartImportCustomizer(source, context,
classNode);
ClassNode mainClassNode = MainClass.get(source.getAST().getClasses());
// Additional auto configuration
for (CompilerAutoConfiguration autoConfiguration : GroovyCompiler.this.compilerAutoConfigurations) {
if (autoConfiguration.matches(classNode)) {
if (GroovyCompiler.this.configuration.isGuessImports()) {
autoConfiguration.applyImports(importCustomizer);
importCustomizer.call(source, context, classNode);
}
if (classNode.equals(mainClassNode)) {
autoConfiguration.applyToMainClass(GroovyCompiler.this.loader,
GroovyCompiler.this.configuration, context, source,
classNode);
}
autoConfiguration.apply(GroovyCompiler.this.loader,
GroovyCompiler.this.configuration, context, source,
classNode);
}
}
importCustomizer.call(source, context, classNode);
}
}
private static class MainClass {
@SuppressWarnings("unchecked")
public static ClassNode get(CompilationUnit source) {
return get(source.getAST().getClasses());
}
public static ClassNode get(List<ClassNode> classes) {
for (ClassNode node : classes) {
if (AstUtils.hasAtLeastOneAnnotation(node, "Enable*AutoConfiguration")) {
return null; // No need to enhance this
}
if (AstUtils.hasAtLeastOneAnnotation(node, "*Controller", "Configuration",
"Component", "*Service", "Repository", "Enable*")) {
return node;
}
}
return (classes.isEmpty() ? null : classes.get(0));
}
}
}