From 5d8ccbacdfb1a01e512b0ce4143afef141d57ac4 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 6 Oct 2015 09:15:48 +0100 Subject: [PATCH] Add a convenience AstTransformation base class for BOMs Plugin authors can extend this class, provide missing methods, and specify a BOM to add to the dependency management lookup (i.e. dependencies by artifactId) --- .../compiler/GenericBomAstTransformation.java | 141 ++++++++++++++++++ .../GenericBomAstTransformationTests.java | 123 +++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GenericBomAstTransformation.java create mode 100644 spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/GenericBomAstTransformationTests.java diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GenericBomAstTransformation.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GenericBomAstTransformation.java new file mode 100644 index 00000000000..70d337dea57 --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GenericBomAstTransformation.java @@ -0,0 +1,141 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.cli.compiler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.PackageNode; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.ListExpression; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.GroovyASTTransformation; +import org.springframework.boot.groovy.DependencyManagementBom; +import org.springframework.core.Ordered; + +/** + * A base class that lets plugin authors easily add additional BOMs to all apps. All the + * dependencies in the bom (and it's transitives) will be added to the dependency + * management lookup, so an app can use just the artifact id (e.g. "spring-jdbc") in a + * @Grab. To install, implement the missing methods and list the class in + * META-INF/services/org.springframework.boot.cli.compiler.SpringBootAstTransformation + * . The {@link #getOrder()} value needs to be before + * {@link DependencyManagementBomTransformation#ORDER}. + * + * @author Dave Syer + * + */ +@GroovyASTTransformation(phase = CompilePhase.CONVERSION) +public abstract class GenericBomAstTransformation + implements SpringBootAstTransformation, Ordered { + + private static ClassNode BOM = ClassHelper.make(DependencyManagementBom.class); + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + for (ASTNode astNode : nodes) { + if (astNode instanceof ModuleNode) { + visitModule((ModuleNode) astNode, getBomModule()); + } + } + } + + /** + * The bom to be added to dependency management in compact form: + * "<groupId>:<artifactId>:<version>" (like in a + * @Grab). + * + * @return the maven co-ordinates of the bom to add + */ + abstract protected String getBomModule(); + + private void visitModule(ModuleNode node, String module) { + addDependencyManagementBom(node, module); + } + + private void addDependencyManagementBom(ModuleNode node, String module) { + AnnotatedNode annotated = getAnnotatedNode(node); + if (annotated != null) { + AnnotationNode bom = getAnnotation(annotated); + List expressions = new ArrayList( + getConstantExpressions(bom.getMember("value"))); + expressions.add(new ConstantExpression(module)); + bom.setMember("value", new ListExpression(expressions)); + } + } + + private AnnotationNode getAnnotation(AnnotatedNode annotated) { + AnnotationNode annotation; + List annotations = annotated.getAnnotations(BOM); + if (annotations.isEmpty()) { + annotation = new AnnotationNode(BOM); + annotated.addAnnotation(annotation); + } + else { + annotation = annotations.get(0); + } + return annotation; + } + + private AnnotatedNode getAnnotatedNode(ModuleNode node) { + PackageNode pkg = node.getPackage(); + if (pkg != null) { + if (!pkg.getAnnotations(BOM).isEmpty()) { + return pkg; + } + } + if (!node.getClasses().isEmpty()) { + ClassNode cls = node.getClasses().get(0); + return cls; + } + return pkg; + } + + private List getConstantExpressions(Expression valueExpression) { + if (valueExpression instanceof ListExpression) { + return getConstantExpressions((ListExpression) valueExpression); + } + if (valueExpression instanceof ConstantExpression + && ((ConstantExpression) valueExpression).getValue() instanceof String) { + return Arrays.asList((ConstantExpression) valueExpression); + } + return Collections.emptyList(); + } + + private List getConstantExpressions( + ListExpression valueExpression) { + List expressions = new ArrayList(); + for (Expression expression : valueExpression.getExpressions()) { + if (expression instanceof ConstantExpression + && ((ConstantExpression) expression).getValue() instanceof String) { + expressions.add((ConstantExpression) expression); + } + } + return expressions; + } + +} diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/GenericBomAstTransformationTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/GenericBomAstTransformationTests.java new file mode 100644 index 00000000000..a51d3562ca0 --- /dev/null +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/GenericBomAstTransformationTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.cli.compiler; + +import java.util.ArrayList; +import java.util.List; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.PackageNode; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.ListExpression; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.io.ReaderSource; +import org.codehaus.groovy.transform.ASTTransformation; +import org.junit.Test; +import org.springframework.boot.groovy.DependencyManagementBom; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@link ResolveDependencyCoordinatesTransformation} + * + * @author Andy Wilkinson + */ +public final class GenericBomAstTransformationTests { + + private final SourceUnit sourceUnit = new SourceUnit((String) null, + (ReaderSource) null, null, null, null); + + private final ModuleNode moduleNode = new ModuleNode(this.sourceUnit); + + private final ASTTransformation transformation = new GenericBomAstTransformation() { + + @Override + public int getOrder() { + return DependencyManagementBomTransformation.ORDER - 10; + } + + @Override + protected String getBomModule() { + return "test:child:1.0.0"; + } + }; + + @Test + public void transformationOfEmptyPackage() { + this.moduleNode.setPackage(new PackageNode("foo")); + this.transformation.visit(new ASTNode[] { this.moduleNode }, this.sourceUnit); + assertEquals("[test:child:1.0.0]", getValue().toString()); + } + + @Test + public void transformationOfClass() { + this.moduleNode.addClass(ClassHelper.make("MyClass")); + this.transformation.visit(new ASTNode[] { this.moduleNode }, this.sourceUnit); + assertEquals("[test:child:1.0.0]", getValue().toString()); + } + + @Test + public void transformationOfClassWithExistingManagedDependencies() { + this.moduleNode.setPackage(new PackageNode("foo")); + ClassNode cls = ClassHelper.make("MyClass"); + this.moduleNode.addClass(cls); + AnnotationNode annotation = new AnnotationNode( + ClassHelper.make(DependencyManagementBom.class)); + annotation.addMember("value", new ConstantExpression("test:parent:1.0.0")); + cls.addAnnotation(annotation); + this.transformation.visit(new ASTNode[] { this.moduleNode }, this.sourceUnit); + assertEquals("[test:parent:1.0.0, test:child:1.0.0]", getValue().toString()); + } + + private List getValue() { + Expression expression = findAnnotation().getMember("value"); + if (expression instanceof ListExpression) { + List list = new ArrayList(); + for (Expression ex : ((ListExpression) expression).getExpressions()) { + list.add((String) ((ConstantExpression) ex).getValue()); + } + return list; + } + else if (expression == null) { + return null; + } + else { + throw new IllegalStateException("Member 'value' is not a ListExpression"); + } + } + + private AnnotationNode findAnnotation() { + PackageNode pkg = this.moduleNode.getPackage(); + ClassNode bom = ClassHelper.make(DependencyManagementBom.class); + if (pkg != null) { + if (!pkg.getAnnotations(bom).isEmpty()) { + return pkg.getAnnotations(bom).get(0); + } + } + if (!this.moduleNode.getClasses().isEmpty()) { + ClassNode cls = this.moduleNode.getClasses().get(0); + return cls.getAnnotations(bom).get(0); + } + throw new IllegalStateException("No package or class node found"); + } + +}