001/*
002 * Copyright 2012-2015 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.lang.reflect.Modifier;
020import java.util.ArrayList;
021
022import org.codehaus.groovy.ast.ASTNode;
023import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
024import org.codehaus.groovy.ast.ClassHelper;
025import org.codehaus.groovy.ast.ClassNode;
026import org.codehaus.groovy.ast.ModuleNode;
027import org.codehaus.groovy.ast.PropertyNode;
028import org.codehaus.groovy.ast.expr.ClosureExpression;
029import org.codehaus.groovy.ast.stmt.BlockStatement;
030import org.codehaus.groovy.control.SourceUnit;
031import org.codehaus.groovy.transform.ASTTransformation;
032
033import org.springframework.core.annotation.Order;
034
035/**
036 * {@link ASTTransformation} to resolve beans declarations inside application source
037 * files. Users only need to define a <code>beans{}</code> DSL element, and this
038 * transformation will remove it and make it accessible to the Spring application via an
039 * interface.
040 *
041 * @author Dave Syer
042 */
043@Order(GroovyBeansTransformation.ORDER)
044public class GroovyBeansTransformation implements ASTTransformation {
045
046        /**
047         * The order of the transformation.
048         */
049        public static final int ORDER = DependencyManagementBomTransformation.ORDER + 200;
050
051        @Override
052        public void visit(ASTNode[] nodes, SourceUnit source) {
053                for (ASTNode node : nodes) {
054                        if (node instanceof ModuleNode) {
055                                ModuleNode module = (ModuleNode) node;
056                                for (ClassNode classNode : new ArrayList<ClassNode>(
057                                                module.getClasses())) {
058                                        if (classNode.isScript()) {
059                                                classNode.visitContents(new ClassVisitor(source, classNode));
060                                        }
061                                }
062                        }
063                }
064        }
065
066        private class ClassVisitor extends ClassCodeVisitorSupport {
067
068                private static final String SOURCE_INTERFACE = "org.springframework.boot.BeanDefinitionLoader.GroovyBeanDefinitionSource";
069
070                private static final String BEANS = "beans";
071
072                private final SourceUnit source;
073
074                private final ClassNode classNode;
075
076                private boolean xformed = false;
077
078                ClassVisitor(SourceUnit source, ClassNode classNode) {
079                        this.source = source;
080                        this.classNode = classNode;
081                }
082
083                @Override
084                protected SourceUnit getSourceUnit() {
085                        return this.source;
086                }
087
088                @Override
089                public void visitBlockStatement(BlockStatement block) {
090                        if (block.isEmpty() || this.xformed) {
091                                return;
092                        }
093                        ClosureExpression closure = beans(block);
094                        if (closure != null) {
095                                // Add a marker interface to the current script
096                                this.classNode.addInterface(ClassHelper.make(SOURCE_INTERFACE));
097                                // Implement the interface by adding a public read-only property with the
098                                // same name as the method in the interface (getBeans). Make it return the
099                                // closure.
100                                this.classNode.addProperty(
101                                                new PropertyNode(BEANS, Modifier.PUBLIC | Modifier.FINAL,
102                                                                ClassHelper.CLOSURE_TYPE.getPlainNodeReference(),
103                                                                this.classNode, closure, null, null));
104                                // Only do this once per class
105                                this.xformed = true;
106                        }
107                }
108
109                /**
110                 * Extract a top-level <code>beans{}</code> closure from inside this block if
111                 * there is one. Removes it from the block at the same time.
112                 * @param block a block statement (class definition)
113                 * @return a beans Closure if one can be found, null otherwise
114                 */
115                private ClosureExpression beans(BlockStatement block) {
116                        return AstUtils.getClosure(block, BEANS, true);
117                }
118
119        }
120
121}