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}