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}