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 0000000000..70d337dea5
--- /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 0000000000..a51d3562ca
--- /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");
+ }
+
+}