001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.cli.compiler; 018 019import java.io.IOException; 020import java.lang.reflect.Field; 021import java.net.URL; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.ServiceLoader; 027 028import groovy.lang.GroovyClassLoader; 029import groovy.lang.GroovyClassLoader.ClassCollector; 030import groovy.lang.GroovyCodeSource; 031import org.codehaus.groovy.ast.ASTNode; 032import org.codehaus.groovy.ast.ClassNode; 033import org.codehaus.groovy.classgen.GeneratorContext; 034import org.codehaus.groovy.control.CompilationFailedException; 035import org.codehaus.groovy.control.CompilationUnit; 036import org.codehaus.groovy.control.CompilePhase; 037import org.codehaus.groovy.control.CompilerConfiguration; 038import org.codehaus.groovy.control.Phases; 039import org.codehaus.groovy.control.SourceUnit; 040import org.codehaus.groovy.control.customizers.CompilationCustomizer; 041import org.codehaus.groovy.control.customizers.ImportCustomizer; 042import org.codehaus.groovy.transform.ASTTransformation; 043import org.codehaus.groovy.transform.ASTTransformationVisitor; 044 045import org.springframework.boot.cli.compiler.dependencies.SpringBootDependenciesDependencyManagement; 046import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine; 047import org.springframework.boot.cli.compiler.grape.AetherGrapeEngineFactory; 048import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext; 049import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller; 050import org.springframework.boot.cli.util.ResourceUtils; 051import org.springframework.core.annotation.AnnotationAwareOrderComparator; 052import org.springframework.util.ClassUtils; 053 054/** 055 * Compiler for Groovy sources. Primarily a simple Facade for 056 * {@link GroovyClassLoader#parseClass(GroovyCodeSource)} with the following additional 057 * features: 058 * <ul> 059 * <li>{@link CompilerAutoConfiguration} strategies will be read from 060 * {@code META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration} 061 * (per the standard java {@link ServiceLoader} contract) and applied during compilation 062 * </li> 063 * 064 * <li>Multiple classes can be returned if the Groovy source defines more than one Class 065 * </li> 066 * 067 * <li>Generated class files can also be loaded using 068 * {@link ClassLoader#getResource(String)}</li> 069 * </ul> 070 * 071 * @author Phillip Webb 072 * @author Dave Syer 073 * @author Andy Wilkinson 074 */ 075public class GroovyCompiler { 076 077 private final GroovyCompilerConfiguration configuration; 078 079 private final ExtendedGroovyClassLoader loader; 080 081 private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations; 082 083 private final List<ASTTransformation> transformations; 084 085 /** 086 * Create a new {@link GroovyCompiler} instance. 087 * @param configuration the compiler configuration 088 */ 089 public GroovyCompiler(GroovyCompilerConfiguration configuration) { 090 091 this.configuration = configuration; 092 this.loader = createLoader(configuration); 093 094 DependencyResolutionContext resolutionContext = new DependencyResolutionContext(); 095 resolutionContext.addDependencyManagement( 096 new SpringBootDependenciesDependencyManagement()); 097 098 AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader, 099 configuration.getRepositoryConfiguration(), resolutionContext, 100 configuration.isQuiet()); 101 102 GrapeEngineInstaller.install(grapeEngine); 103 104 this.loader.getConfiguration() 105 .addCompilationCustomizers(new CompilerAutoConfigureCustomizer()); 106 if (configuration.isAutoconfigure()) { 107 this.compilerAutoConfigurations = ServiceLoader 108 .load(CompilerAutoConfiguration.class); 109 } 110 else { 111 this.compilerAutoConfigurations = Collections.emptySet(); 112 } 113 114 this.transformations = new ArrayList<>(); 115 this.transformations 116 .add(new DependencyManagementBomTransformation(resolutionContext)); 117 this.transformations.add(new DependencyAutoConfigurationTransformation( 118 this.loader, resolutionContext, this.compilerAutoConfigurations)); 119 this.transformations.add(new GroovyBeansTransformation()); 120 if (this.configuration.isGuessDependencies()) { 121 this.transformations.add( 122 new ResolveDependencyCoordinatesTransformation(resolutionContext)); 123 } 124 for (ASTTransformation transformation : ServiceLoader 125 .load(SpringBootAstTransformation.class)) { 126 this.transformations.add(transformation); 127 } 128 this.transformations.sort(AnnotationAwareOrderComparator.INSTANCE); 129 } 130 131 /** 132 * Return a mutable list of the {@link ASTTransformation}s to be applied during 133 * {@link #compile(String...)}. 134 * @return the AST transformations to apply 135 */ 136 public List<ASTTransformation> getAstTransformations() { 137 return this.transformations; 138 } 139 140 public ExtendedGroovyClassLoader getLoader() { 141 return this.loader; 142 } 143 144 private ExtendedGroovyClassLoader createLoader( 145 GroovyCompilerConfiguration configuration) { 146 147 ExtendedGroovyClassLoader loader = new ExtendedGroovyClassLoader( 148 configuration.getScope()); 149 150 for (URL url : getExistingUrls()) { 151 loader.addURL(url); 152 } 153 154 for (String classpath : configuration.getClasspath()) { 155 loader.addClasspath(classpath); 156 } 157 158 return loader; 159 } 160 161 private URL[] getExistingUrls() { 162 ClassLoader tccl = Thread.currentThread().getContextClassLoader(); 163 if (tccl instanceof ExtendedGroovyClassLoader) { 164 return ((ExtendedGroovyClassLoader) tccl).getURLs(); 165 } 166 else { 167 return new URL[0]; 168 } 169 } 170 171 public void addCompilationCustomizers(CompilationCustomizer... customizers) { 172 this.loader.getConfiguration().addCompilationCustomizers(customizers); 173 } 174 175 /** 176 * Compile the specified Groovy sources, applying any 177 * {@link CompilerAutoConfiguration}s. All classes defined in the sources will be 178 * returned from this method. 179 * @param sources the sources to compile 180 * @return compiled classes 181 * @throws CompilationFailedException in case of compilation failures 182 * @throws IOException in case of I/O errors 183 * @throws CompilationFailedException in case of compilation errors 184 */ 185 public Class<?>[] compile(String... sources) 186 throws CompilationFailedException, IOException { 187 188 this.loader.clearCache(); 189 List<Class<?>> classes = new ArrayList<>(); 190 191 CompilerConfiguration configuration = this.loader.getConfiguration(); 192 193 CompilationUnit compilationUnit = new CompilationUnit(configuration, null, 194 this.loader); 195 ClassCollector collector = this.loader.createCollector(compilationUnit, null); 196 compilationUnit.setClassgenCallback(collector); 197 198 for (String source : sources) { 199 List<String> paths = ResourceUtils.getUrls(source, this.loader); 200 for (String path : paths) { 201 compilationUnit.addSource(new URL(path)); 202 } 203 } 204 205 addAstTransformations(compilationUnit); 206 207 compilationUnit.compile(Phases.CLASS_GENERATION); 208 for (Object loadedClass : collector.getLoadedClasses()) { 209 classes.add((Class<?>) loadedClass); 210 } 211 ClassNode mainClassNode = MainClass.get(compilationUnit); 212 213 Class<?> mainClass = null; 214 for (Class<?> loadedClass : classes) { 215 if (mainClassNode.getName().equals(loadedClass.getName())) { 216 mainClass = loadedClass; 217 } 218 } 219 if (mainClass != null) { 220 classes.remove(mainClass); 221 classes.add(0, mainClass); 222 } 223 224 return ClassUtils.toClassArray(classes); 225 } 226 227 @SuppressWarnings("rawtypes") 228 private void addAstTransformations(CompilationUnit compilationUnit) { 229 LinkedList[] phaseOperations = getPhaseOperations(compilationUnit); 230 processConversionOperations(phaseOperations[Phases.CONVERSION]); 231 } 232 233 @SuppressWarnings("rawtypes") 234 private LinkedList[] getPhaseOperations(CompilationUnit compilationUnit) { 235 try { 236 Field field = CompilationUnit.class.getDeclaredField("phaseOperations"); 237 field.setAccessible(true); 238 return (LinkedList[]) field.get(compilationUnit); 239 } 240 catch (Exception ex) { 241 throw new IllegalStateException( 242 "Phase operations not available from compilation unit"); 243 } 244 } 245 246 @SuppressWarnings({ "rawtypes", "unchecked" }) 247 private void processConversionOperations(LinkedList conversionOperations) { 248 int index = getIndexOfASTTransformationVisitor(conversionOperations); 249 conversionOperations.add(index, new CompilationUnit.SourceUnitOperation() { 250 @Override 251 public void call(SourceUnit source) throws CompilationFailedException { 252 ASTNode[] nodes = new ASTNode[] { source.getAST() }; 253 for (ASTTransformation transformation : GroovyCompiler.this.transformations) { 254 transformation.visit(nodes, source); 255 } 256 } 257 }); 258 } 259 260 private int getIndexOfASTTransformationVisitor(List<?> conversionOperations) { 261 for (int index = 0; index < conversionOperations.size(); index++) { 262 if (conversionOperations.get(index).getClass().getName() 263 .startsWith(ASTTransformationVisitor.class.getName())) { 264 return index; 265 } 266 } 267 return conversionOperations.size(); 268 } 269 270 /** 271 * {@link CompilationCustomizer} to call {@link CompilerAutoConfiguration}s. 272 */ 273 private class CompilerAutoConfigureCustomizer extends CompilationCustomizer { 274 275 CompilerAutoConfigureCustomizer() { 276 super(CompilePhase.CONVERSION); 277 } 278 279 @Override 280 public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) 281 throws CompilationFailedException { 282 283 ImportCustomizer importCustomizer = new SmartImportCustomizer(source); 284 ClassNode mainClassNode = MainClass.get(source.getAST().getClasses()); 285 286 // Additional auto configuration 287 for (CompilerAutoConfiguration autoConfiguration : GroovyCompiler.this.compilerAutoConfigurations) { 288 if (autoConfiguration.matches(classNode)) { 289 if (GroovyCompiler.this.configuration.isGuessImports()) { 290 autoConfiguration.applyImports(importCustomizer); 291 importCustomizer.call(source, context, classNode); 292 } 293 if (classNode.equals(mainClassNode)) { 294 autoConfiguration.applyToMainClass(GroovyCompiler.this.loader, 295 GroovyCompiler.this.configuration, context, source, 296 classNode); 297 } 298 autoConfiguration.apply(GroovyCompiler.this.loader, 299 GroovyCompiler.this.configuration, context, source, 300 classNode); 301 } 302 } 303 importCustomizer.call(source, context, classNode); 304 } 305 306 } 307 308 private static class MainClass { 309 310 @SuppressWarnings("unchecked") 311 public static ClassNode get(CompilationUnit source) { 312 return get(source.getAST().getClasses()); 313 } 314 315 public static ClassNode get(List<ClassNode> classes) { 316 for (ClassNode node : classes) { 317 if (AstUtils.hasAtLeastOneAnnotation(node, "Enable*AutoConfiguration")) { 318 return null; // No need to enhance this 319 } 320 if (AstUtils.hasAtLeastOneAnnotation(node, "*Controller", "Configuration", 321 "Component", "*Service", "Repository", "Enable*")) { 322 return node; 323 } 324 } 325 return classes.isEmpty() ? null : classes.get(0); 326 } 327 328 } 329 330}