001/* 002 * Copyright 2012-2017 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; 052 053/** 054 * Compiler for Groovy sources. Primarily a simple Facade for 055 * {@link GroovyClassLoader#parseClass(GroovyCodeSource)} with the following additional 056 * features: 057 * <ul> 058 * <li>{@link CompilerAutoConfiguration} strategies will be read from 059 * {@code META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration} 060 * (per the standard java {@link ServiceLoader} contract) and applied during compilation 061 * </li> 062 * 063 * <li>Multiple classes can be returned if the Groovy source defines more than one Class 064 * </li> 065 * 066 * <li>Generated class files can also be loaded using 067 * {@link ClassLoader#getResource(String)}</li> 068 * </ul> 069 * 070 * @author Phillip Webb 071 * @author Dave Syer 072 * @author Andy Wilkinson 073 */ 074public class GroovyCompiler { 075 076 private final GroovyCompilerConfiguration configuration; 077 078 private final ExtendedGroovyClassLoader loader; 079 080 private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations; 081 082 private final List<ASTTransformation> transformations; 083 084 /** 085 * Create a new {@link GroovyCompiler} instance. 086 * @param configuration the compiler configuration 087 */ 088 public GroovyCompiler(final GroovyCompilerConfiguration configuration) { 089 090 this.configuration = configuration; 091 this.loader = createLoader(configuration); 092 093 DependencyResolutionContext resolutionContext = new DependencyResolutionContext(); 094 resolutionContext.addDependencyManagement( 095 new SpringBootDependenciesDependencyManagement()); 096 097 AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader, 098 configuration.getRepositoryConfiguration(), resolutionContext, 099 configuration.isQuiet()); 100 101 GrapeEngineInstaller.install(grapeEngine); 102 103 this.loader.getConfiguration() 104 .addCompilationCustomizers(new CompilerAutoConfigureCustomizer()); 105 if (configuration.isAutoconfigure()) { 106 this.compilerAutoConfigurations = ServiceLoader 107 .load(CompilerAutoConfiguration.class); 108 } 109 else { 110 this.compilerAutoConfigurations = Collections.emptySet(); 111 } 112 113 this.transformations = new ArrayList<ASTTransformation>(); 114 this.transformations 115 .add(new DependencyManagementBomTransformation(resolutionContext)); 116 this.transformations.add(new DependencyAutoConfigurationTransformation( 117 this.loader, resolutionContext, this.compilerAutoConfigurations)); 118 this.transformations.add(new GroovyBeansTransformation()); 119 if (this.configuration.isGuessDependencies()) { 120 this.transformations.add( 121 new ResolveDependencyCoordinatesTransformation(resolutionContext)); 122 } 123 for (ASTTransformation transformation : ServiceLoader 124 .load(SpringBootAstTransformation.class)) { 125 this.transformations.add(transformation); 126 } 127 Collections.sort(this.transformations, AnnotationAwareOrderComparator.INSTANCE); 128 } 129 130 /** 131 * Return a mutable list of the {@link ASTTransformation}s to be applied during 132 * {@link #compile(String...)}. 133 * @return the AST transformations to apply 134 */ 135 public List<ASTTransformation> getAstTransformations() { 136 return this.transformations; 137 } 138 139 public ExtendedGroovyClassLoader getLoader() { 140 return this.loader; 141 } 142 143 private ExtendedGroovyClassLoader createLoader( 144 GroovyCompilerConfiguration configuration) { 145 146 ExtendedGroovyClassLoader loader = new ExtendedGroovyClassLoader( 147 configuration.getScope()); 148 149 for (URL url : getExistingUrls()) { 150 loader.addURL(url); 151 } 152 153 for (String classpath : configuration.getClasspath()) { 154 loader.addClasspath(classpath); 155 } 156 157 return loader; 158 } 159 160 private URL[] getExistingUrls() { 161 ClassLoader tccl = Thread.currentThread().getContextClassLoader(); 162 if (tccl instanceof ExtendedGroovyClassLoader) { 163 return ((ExtendedGroovyClassLoader) tccl).getURLs(); 164 } 165 else { 166 return new URL[0]; 167 } 168 } 169 170 public void addCompilationCustomizers(CompilationCustomizer... customizers) { 171 this.loader.getConfiguration().addCompilationCustomizers(customizers); 172 } 173 174 /** 175 * Compile the specified Groovy sources, applying any 176 * {@link CompilerAutoConfiguration}s. All classes defined in the sources will be 177 * returned from this method. 178 * @param sources the sources to compile 179 * @return compiled classes 180 * @throws CompilationFailedException in case of compilation failures 181 * @throws IOException in case of I/O errors 182 * @throws CompilationFailedException in case of compilation errors 183 */ 184 public Class<?>[] compile(String... sources) 185 throws CompilationFailedException, IOException { 186 187 this.loader.clearCache(); 188 List<Class<?>> classes = new ArrayList<Class<?>>(); 189 190 CompilerConfiguration configuration = this.loader.getConfiguration(); 191 192 CompilationUnit compilationUnit = new CompilationUnit(configuration, null, 193 this.loader); 194 ClassCollector collector = this.loader.createCollector(compilationUnit, null); 195 compilationUnit.setClassgenCallback(collector); 196 197 for (String source : sources) { 198 List<String> paths = ResourceUtils.getUrls(source, this.loader); 199 for (String path : paths) { 200 compilationUnit.addSource(new URL(path)); 201 } 202 } 203 204 addAstTransformations(compilationUnit); 205 206 compilationUnit.compile(Phases.CLASS_GENERATION); 207 for (Object loadedClass : collector.getLoadedClasses()) { 208 classes.add((Class<?>) loadedClass); 209 } 210 ClassNode mainClassNode = MainClass.get(compilationUnit); 211 212 Class<?> mainClass = null; 213 for (Class<?> loadedClass : classes) { 214 if (mainClassNode.getName().equals(loadedClass.getName())) { 215 mainClass = loadedClass; 216 } 217 } 218 if (mainClass != null) { 219 classes.remove(mainClass); 220 classes.add(0, mainClass); 221 } 222 223 return classes.toArray(new Class<?>[classes.size()]); 224 } 225 226 @SuppressWarnings("rawtypes") 227 private void addAstTransformations(CompilationUnit compilationUnit) { 228 LinkedList[] phaseOperations = getPhaseOperations(compilationUnit); 229 processConversionOperations(phaseOperations[Phases.CONVERSION]); 230 } 231 232 @SuppressWarnings("rawtypes") 233 private LinkedList[] getPhaseOperations(CompilationUnit compilationUnit) { 234 try { 235 Field field = CompilationUnit.class.getDeclaredField("phaseOperations"); 236 field.setAccessible(true); 237 LinkedList[] phaseOperations = (LinkedList[]) field.get(compilationUnit); 238 return phaseOperations; 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(LinkedList<?> 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, context, 284 classNode); 285 ClassNode mainClassNode = MainClass.get(source.getAST().getClasses()); 286 287 // Additional auto configuration 288 for (CompilerAutoConfiguration autoConfiguration : GroovyCompiler.this.compilerAutoConfigurations) { 289 if (autoConfiguration.matches(classNode)) { 290 if (GroovyCompiler.this.configuration.isGuessImports()) { 291 autoConfiguration.applyImports(importCustomizer); 292 importCustomizer.call(source, context, classNode); 293 } 294 if (classNode.equals(mainClassNode)) { 295 autoConfiguration.applyToMainClass(GroovyCompiler.this.loader, 296 GroovyCompiler.this.configuration, context, source, 297 classNode); 298 } 299 autoConfiguration.apply(GroovyCompiler.this.loader, 300 GroovyCompiler.this.configuration, context, source, 301 classNode); 302 } 303 } 304 importCustomizer.call(source, context, classNode); 305 } 306 307 } 308 309 private static class MainClass { 310 311 @SuppressWarnings("unchecked") 312 public static ClassNode get(CompilationUnit source) { 313 return get(source.getAST().getClasses()); 314 } 315 316 public static ClassNode get(List<ClassNode> classes) { 317 for (ClassNode node : classes) { 318 if (AstUtils.hasAtLeastOneAnnotation(node, "Enable*AutoConfiguration")) { 319 return null; // No need to enhance this 320 } 321 if (AstUtils.hasAtLeastOneAnnotation(node, "*Controller", "Configuration", 322 "Component", "*Service", "Repository", "Enable*")) { 323 return node; 324 } 325 } 326 return (classes.isEmpty() ? null : classes.get(0)); 327 } 328 329 } 330 331}