Allow custom dependency metadata to be used with the CLI

Add support for a new annotation, @GrabMetadata, that can be used
to provide the coordinates of one or more properties files, such as
the one published by Spring IO Platform, as a source of dependency
metadata. For example:

@GrabMetadata("com.example:metadata:1.0.0")

The referenced properties files must be in the format
group:module=version.

Limitations:

 - Only a single @GrabMetadata annotation is supported
 - The referenced properties file must be accessible in one of the
   default repositories, i.e. it cannot be accessed in a repository
   that's added using @GrabResolver

Closes #814
pull/890/merge
Andy Wilkinson 11 years ago
parent 3f498a4803
commit d2fc80b818

@ -0,0 +1,125 @@
/*
* Copyright 2012-2014 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 java.util.Map;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
/**
* A base class for {@link ASTTransformation AST transformations} that are solely
* interested in {@link AnnotatedNode AnnotatedNodes}.
*
* @author Andy Wilkinson
* @since 1.1.0
*/
public abstract class AnnotatedNodeASTTransformation implements ASTTransformation {
private final Set<String> interestingAnnotationNames;
private List<AnnotationNode> annotationNodes = new ArrayList<AnnotationNode>();
private SourceUnit sourceUnit;
protected AnnotatedNodeASTTransformation(Set<String> interestingAnnotationNames) {
this.interestingAnnotationNames = interestingAnnotationNames;
}
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
this.sourceUnit = source;
ClassVisitor classVisitor = new ClassVisitor(source);
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
ModuleNode module = (ModuleNode) node;
visitAnnotatedNode(module.getPackage());
for (ImportNode importNode : module.getImports()) {
visitAnnotatedNode(importNode);
}
for (ImportNode importNode : module.getStarImports()) {
visitAnnotatedNode(importNode);
}
for (Map.Entry<String, ImportNode> entry : module.getStaticImports()
.entrySet()) {
visitAnnotatedNode(entry.getValue());
}
for (Map.Entry<String, ImportNode> entry : module.getStaticStarImports()
.entrySet()) {
visitAnnotatedNode(entry.getValue());
}
for (ClassNode classNode : module.getClasses()) {
visitAnnotatedNode(classNode);
classNode.visitContents(classVisitor);
}
}
}
processAnnotationNodes(this.annotationNodes);
}
protected SourceUnit getSourceUnit() {
return this.sourceUnit;
}
protected abstract void processAnnotationNodes(List<AnnotationNode> annotationNodes);
private void visitAnnotatedNode(AnnotatedNode annotatedNode) {
if (annotatedNode != null) {
for (AnnotationNode annotationNode : annotatedNode.getAnnotations()) {
if (this.interestingAnnotationNames.contains(annotationNode
.getClassNode().getName())) {
this.annotationNodes.add(annotationNode);
}
}
}
}
private class ClassVisitor extends ClassCodeVisitorSupport {
private final SourceUnit source;
public ClassVisitor(SourceUnit source) {
this.source = source;
}
@Override
protected SourceUnit getSourceUnit() {
return this.source;
}
@Override
public void visitAnnotations(AnnotatedNode node) {
visitAnnotatedNode(node);
}
}
}

@ -23,13 +23,12 @@ import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
/**
* {@link ASTTransformation} to apply
* {@link CompilerAutoConfiguration#applyDependencies(DependencyCustomizer) dependency
* auto-configuration}.
*
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
@ -38,15 +37,15 @@ public class DependencyAutoConfigurationTransformation implements ASTTransformat
private final GroovyClassLoader loader;
private final ArtifactCoordinatesResolver coordinatesResolver;
private final DependencyResolutionContext dependencyResolutionContext;
private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations;
public DependencyAutoConfigurationTransformation(GroovyClassLoader loader,
ArtifactCoordinatesResolver coordinatesResolver,
DependencyResolutionContext dependencyResolutionContext,
Iterable<CompilerAutoConfiguration> compilerAutoConfigurations) {
this.loader = loader;
this.coordinatesResolver = coordinatesResolver;
this.dependencyResolutionContext = dependencyResolutionContext;
this.compilerAutoConfigurations = compilerAutoConfigurations;
}
@ -62,7 +61,7 @@ public class DependencyAutoConfigurationTransformation implements ASTTransformat
private void visitModule(ModuleNode module) {
DependencyCustomizer dependencies = new DependencyCustomizer(this.loader, module,
this.coordinatesResolver);
this.dependencyResolutionContext);
for (ClassNode classNode : module.getClasses()) {
for (CompilerAutoConfiguration autoConfiguration : this.compilerAutoConfigurations) {
if (autoConfiguration.matches(classNode)) {

@ -32,7 +32,7 @@ import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesRes
* <p>
* This class provides a fluent API for conditionally adding dependencies. For example:
* {@code dependencies.ifMissing("com.corp.SomeClass").add(module)}.
*
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
@ -42,17 +42,17 @@ public class DependencyCustomizer {
private final ClassNode classNode;
private final ArtifactCoordinatesResolver coordinatesResolver;
private final DependencyResolutionContext dependencyResolutionContext;
/**
* Create a new {@link DependencyCustomizer} instance.
* @param loader
*/
public DependencyCustomizer(GroovyClassLoader loader, ModuleNode moduleNode,
ArtifactCoordinatesResolver coordinatesResolver) {
DependencyResolutionContext dependencyResolutionContext) {
this.loader = loader;
this.classNode = moduleNode.getClasses().get(0);
this.coordinatesResolver = coordinatesResolver;
this.dependencyResolutionContext = dependencyResolutionContext;
}
/**
@ -62,7 +62,7 @@ public class DependencyCustomizer {
protected DependencyCustomizer(DependencyCustomizer parent) {
this.loader = parent.loader;
this.classNode = parent.classNode;
this.coordinatesResolver = parent.coordinatesResolver;
this.dependencyResolutionContext = parent.dependencyResolutionContext;
}
public String getVersion(String artifactId) {
@ -71,7 +71,8 @@ public class DependencyCustomizer {
}
public String getVersion(String artifactId, String defaultVersion) {
String version = this.coordinatesResolver.getVersion(artifactId);
String version = this.dependencyResolutionContext
.getArtifactCoordinatesResolver().getVersion(artifactId);
if (version == null) {
version = defaultVersion;
}
@ -201,9 +202,11 @@ public class DependencyCustomizer {
*/
public DependencyCustomizer add(String module, boolean transitive) {
if (canAdd()) {
ArtifactCoordinatesResolver artifactCoordinatesResolver = this.dependencyResolutionContext
.getArtifactCoordinatesResolver();
this.classNode.addAnnotation(createGrabAnnotation(
this.coordinatesResolver.getGroupId(module), module,
this.coordinatesResolver.getVersion(module), transitive));
artifactCoordinatesResolver.getGroupId(module), module,
artifactCoordinatesResolver.getVersion(module), transitive));
}
return this;
}

@ -0,0 +1,62 @@
/*
* Copyright 2012-2014 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.eclipse.aether.graph.Dependency;
import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
import org.springframework.boot.cli.compiler.dependencies.ManagedDependenciesArtifactCoordinatesResolver;
import org.springframework.boot.cli.compiler.grape.ManagedDependenciesFactory;
import org.springframework.boot.dependency.tools.ManagedDependencies;
/**
* @author Andy Wilkinson
* @since 1.1.0
*/
public class DependencyResolutionContext {
private ArtifactCoordinatesResolver artifactCoordinatesResolver;
private List<Dependency> managedDependencies = new ArrayList<Dependency>();
public DependencyResolutionContext() {
this(new ManagedDependenciesArtifactCoordinatesResolver());
}
DependencyResolutionContext(ArtifactCoordinatesResolver artifactCoordinatesResolver) {
this.artifactCoordinatesResolver = artifactCoordinatesResolver;
}
void setManagedDependencies(ManagedDependencies managedDependencies) {
this.artifactCoordinatesResolver = new ManagedDependenciesArtifactCoordinatesResolver(
managedDependencies);
this.managedDependencies = new ArrayList<Dependency>(
new ManagedDependenciesFactory(managedDependencies)
.getManagedDependencies());
}
ArtifactCoordinatesResolver getArtifactCoordinatesResolver() {
return this.artifactCoordinatesResolver;
}
public List<Dependency> getManagedDependencies() {
return this.managedDependencies;
}
}

@ -0,0 +1,184 @@
/*
* Copyright 2012-2014 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 groovy.grape.Grape;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotationNode;
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.messages.Message;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.dependency.tools.ManagedDependencies;
import org.springframework.boot.dependency.tools.PropertiesFileManagedDependencies;
import org.springframework.boot.dependency.tools.VersionManagedDependencies;
import org.springframework.boot.groovy.GrabMetadata;
/**
* {@link ASTTransformation} for processing {@link GrabMetadata @GrabMetadata}
*
* @author Andy Wilkinson
* @since 1.1.0
*/
public class GrabMetadataTransformation extends AnnotatedNodeASTTransformation {
private static final Set<String> GRAB_METADATA_ANNOTATION_NAMES = Collections
.unmodifiableSet(new HashSet<String>(Arrays.asList(
GrabMetadata.class.getName(), GrabMetadata.class.getSimpleName())));
private final DependencyResolutionContext resolutionContext;
public GrabMetadataTransformation(DependencyResolutionContext resolutionContext) {
super(GRAB_METADATA_ANNOTATION_NAMES);
this.resolutionContext = resolutionContext;
}
@Override
protected void processAnnotationNodes(List<AnnotationNode> annotationNodes) {
if (!annotationNodes.isEmpty()) {
if (annotationNodes.size() > 1) {
for (AnnotationNode annotationNode : annotationNodes) {
handleDuplicateGrabMetadataAnnotation(annotationNode);
}
}
else {
processGrabMetadataAnnotation(annotationNodes.get(0));
}
}
}
private void processGrabMetadataAnnotation(AnnotationNode annotationNode) {
Expression valueExpression = annotationNode.getMember("value");
List<Map<String, String>> metadataDependencies = createDependencyMaps(valueExpression);
updateArtifactCoordinatesResolver(metadataDependencies);
}
private List<Map<String, String>> createDependencyMaps(Expression valueExpression) {
Map<String, String> dependency = null;
List<ConstantExpression> constantExpressions = new ArrayList<ConstantExpression>();
if (valueExpression instanceof ListExpression) {
ListExpression listExpression = (ListExpression) valueExpression;
for (Expression expression : listExpression.getExpressions()) {
if (expression instanceof ConstantExpression
&& ((ConstantExpression) expression).getValue() instanceof String) {
constantExpressions.add((ConstantExpression) expression);
}
else {
reportError(
"Each entry in the array must be an inline string constant",
expression);
}
}
}
else if (valueExpression instanceof ConstantExpression
&& ((ConstantExpression) valueExpression).getValue() instanceof String) {
constantExpressions = Arrays.asList((ConstantExpression) valueExpression);
}
else {
reportError(
"@GrabMetadata requires an inline constant that is a string or a string array",
valueExpression);
}
List<Map<String, String>> dependencies = new ArrayList<Map<String, String>>(
constantExpressions.size());
for (ConstantExpression expression : constantExpressions) {
Object value = expression.getValue();
if (value instanceof String) {
String[] components = ((String) expression.getValue()).split(":");
if (components.length == 3) {
dependency = new HashMap<String, String>();
dependency.put("group", components[0]);
dependency.put("module", components[1]);
dependency.put("version", components[2]);
dependency.put("type", "properties");
dependencies.add(dependency);
}
else {
handleMalformedDependency(expression);
}
}
}
return dependencies;
}
private void handleMalformedDependency(Expression expression) {
Message message = createSyntaxErrorMessage(
"The string must be of the form \"group:module:version\"\n", expression);
getSourceUnit().getErrorCollector().addErrorAndContinue(message);
}
private void updateArtifactCoordinatesResolver(
List<Map<String, String>> metadataDependencies) {
URI[] uris = Grape.getInstance().resolve(null,
metadataDependencies.toArray(new Map[metadataDependencies.size()]));
List<ManagedDependencies> managedDependencies = new ArrayList<ManagedDependencies>(
uris.length);
for (URI uri : uris) {
try {
managedDependencies.add(new PropertiesFileManagedDependencies(uri.toURL()
.openStream()));
}
catch (IOException e) {
throw new IllegalStateException("Failed to parse '" + uris[0]
+ "'. Is it a valid properties file?");
}
}
this.resolutionContext.setManagedDependencies(new VersionManagedDependencies(
managedDependencies));
}
private void handleDuplicateGrabMetadataAnnotation(AnnotationNode annotationNode) {
Message message = createSyntaxErrorMessage(
"Duplicate @GrabMetadata annotation. It must be declared at most once.",
annotationNode);
getSourceUnit().getErrorCollector().addErrorAndContinue(message);
}
private void reportError(String message, ASTNode node) {
getSourceUnit().getErrorCollector().addErrorAndContinue(
createSyntaxErrorMessage(message, node));
}
private Message createSyntaxErrorMessage(String message, ASTNode node) {
return new SyntaxErrorMessage(new SyntaxException(message, node.getLineNumber(),
node.getColumnNumber(), node.getLastLineNumber(),
node.getLastColumnNumber()), getSourceUnit());
}
}

@ -43,8 +43,6 @@ import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.ASTTransformationVisitor;
import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
import org.springframework.boot.cli.compiler.dependencies.ManagedDependenciesArtifactCoordinatesResolver;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngineFactory;
import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller;
@ -58,21 +56,19 @@ import org.springframework.boot.cli.util.ResourceUtils;
* <li>{@link CompilerAutoConfiguration} strategies will be read from
* <code>META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration</code>
* (per the standard java {@link ServiceLoader} contract) and applied during compilation</li>
*
*
* <li>Multiple classes can be returned if the Groovy source defines more than one Class</li>
*
*
* <li>Generated class files can also be loaded using
* {@link ClassLoader#getResource(String)}</li>
* </ul>
*
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
*/
public class GroovyCompiler {
private final ArtifactCoordinatesResolver coordinatesResolver;
private final GroovyCompilerConfiguration configuration;
private final ExtendedGroovyClassLoader loader;
@ -90,10 +86,10 @@ public class GroovyCompiler {
this.configuration = configuration;
this.loader = createLoader(configuration);
this.coordinatesResolver = new ManagedDependenciesArtifactCoordinatesResolver();
DependencyResolutionContext resolutionContext = new DependencyResolutionContext();
AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader,
configuration.getRepositoryConfiguration());
configuration.getRepositoryConfiguration(), resolutionContext);
GrapeEngineInstaller.install(grapeEngine);
@ -108,12 +104,13 @@ public class GroovyCompiler {
}
this.transformations = new ArrayList<ASTTransformation>();
this.transformations.add(new GrabMetadataTransformation(resolutionContext));
this.transformations.add(new DependencyAutoConfigurationTransformation(
this.loader, this.coordinatesResolver, this.compilerAutoConfigurations));
this.loader, resolutionContext, this.compilerAutoConfigurations));
this.transformations.add(new GroovyBeansTransformation());
if (this.configuration.isGuessDependencies()) {
this.transformations.add(new ResolveDependencyCoordinatesTransformation(
this.coordinatesResolver));
resolutionContext));
}
}
@ -170,7 +167,7 @@ public class GroovyCompiler {
* @throws IOException
*/
public Class<?>[] compile(String... sources) throws CompilationFailedException,
IOException {
IOException {
this.loader.clearCache();
List<Class<?>> classes = new ArrayList<Class<?>>();
@ -290,9 +287,9 @@ public class GroovyCompiler {
classNode);
}
autoConfiguration
.apply(GroovyCompiler.this.loader,
GroovyCompiler.this.configuration, context, source,
classNode);
.apply(GroovyCompiler.this.loader,
GroovyCompiler.this.configuration, context, source,
classNode);
}
}
importCustomizer.call(source, context, classNode);

@ -21,81 +21,39 @@ import groovy.lang.Grab;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.List;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
/**
* {@link ASTTransformation} to resolve {@link Grab} artifact coordinates.
*
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class ResolveDependencyCoordinatesTransformation implements ASTTransformation {
public class ResolveDependencyCoordinatesTransformation extends
AnnotatedNodeASTTransformation {
private static final Set<String> GRAB_ANNOTATION_NAMES = Collections
.unmodifiableSet(new HashSet<String>(Arrays.asList(Grab.class.getName(),
Grab.class.getSimpleName())));
private final ArtifactCoordinatesResolver coordinatesResolver;
private final DependencyResolutionContext resolutionContext;
public ResolveDependencyCoordinatesTransformation(
ArtifactCoordinatesResolver coordinatesResolver) {
this.coordinatesResolver = coordinatesResolver;
DependencyResolutionContext resolutionContext) {
super(GRAB_ANNOTATION_NAMES);
this.resolutionContext = resolutionContext;
}
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
ClassVisitor classVisitor = new ClassVisitor(source);
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
ModuleNode module = (ModuleNode) node;
visitAnnotatedNode(module.getPackage());
for (ImportNode importNode : module.getImports()) {
visitAnnotatedNode(importNode);
}
for (ImportNode importNode : module.getStarImports()) {
visitAnnotatedNode(importNode);
}
for (Map.Entry<String, ImportNode> entry : module.getStaticImports()
.entrySet()) {
visitAnnotatedNode(entry.getValue());
}
for (Map.Entry<String, ImportNode> entry : module.getStaticStarImports()
.entrySet()) {
visitAnnotatedNode(entry.getValue());
}
for (ClassNode classNode : module.getClasses()) {
visitAnnotatedNode(classNode);
classNode.visitContents(classVisitor);
}
}
}
}
private void visitAnnotatedNode(AnnotatedNode annotatedNode) {
if (annotatedNode != null) {
for (AnnotationNode annotationNode : annotatedNode.getAnnotations()) {
if (GRAB_ANNOTATION_NAMES.contains(annotationNode.getClassNode()
.getName())) {
transformGrabAnnotation(annotationNode);
}
}
protected void processAnnotationNodes(List<AnnotationNode> annotationNodes) {
for (AnnotationNode annotationNode : annotationNodes) {
transformGrabAnnotation(annotationNode);
}
}
@ -129,10 +87,12 @@ public class ResolveDependencyCoordinatesTransformation implements ASTTransforma
module = (String) ((ConstantExpression) expression).getValue();
}
if (annotation.getMember("group") == null) {
setMember(annotation, "group", this.coordinatesResolver.getGroupId(module));
setMember(annotation, "group", this.resolutionContext
.getArtifactCoordinatesResolver().getGroupId(module));
}
if (annotation.getMember("version") == null) {
setMember(annotation, "version", this.coordinatesResolver.getVersion(module));
setMember(annotation, "version", this.resolutionContext
.getArtifactCoordinatesResolver().getVersion(module));
}
}
@ -140,24 +100,4 @@ public class ResolveDependencyCoordinatesTransformation implements ASTTransforma
ConstantExpression expression = new ConstantExpression(value);
annotation.setMember(name, expression);
}
private class ClassVisitor extends ClassCodeVisitorSupport {
private final SourceUnit source;
public ClassVisitor(SourceUnit source) {
this.source = source;
}
@Override
protected SourceUnit getSourceUnit() {
return this.source;
}
@Override
public void visitAnnotations(AnnotatedNode node) {
visitAnnotatedNode(node);
}
}
}

@ -30,7 +30,7 @@ import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
/**
* {@link CompilerAutoConfiguration} for Spring.
*
*
* @author Dave Syer
* @author Phillip Webb
*/
@ -39,7 +39,7 @@ public class SpringBootCompilerAutoConfiguration extends CompilerAutoConfigurati
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies.ifAnyMissingClasses("org.springframework.boot.SpringApplication")
.add("spring-boot-starter");
.add("spring-boot-starter");
}
@Override
@ -63,7 +63,8 @@ public class SpringBootCompilerAutoConfiguration extends CompilerAutoConfigurati
"org.springframework.core.annotation.Order",
"org.springframework.core.io.ResourceLoader",
"org.springframework.boot.CommandLineRunner",
"org.springframework.boot.autoconfigure.EnableAutoConfiguration");
"org.springframework.boot.autoconfigure.EnableAutoConfiguration",
"org.springframework.boot.groovy.GrabMetadata");
imports.addStarImports("org.springframework.stereotype",
"org.springframework.scheduling.annotation");
}

@ -22,7 +22,7 @@ import org.springframework.boot.dependency.tools.VersionManagedDependencies;
/**
* {@link ArtifactCoordinatesResolver} backed by {@link ManagedDependencies}.
*
*
* @author Phillip Webb
*/
public class ManagedDependenciesArtifactCoordinatesResolver implements
@ -34,7 +34,7 @@ public class ManagedDependenciesArtifactCoordinatesResolver implements
this(new VersionManagedDependencies());
}
ManagedDependenciesArtifactCoordinatesResolver(ManagedDependencies dependencies) {
public ManagedDependenciesArtifactCoordinatesResolver(ManagedDependencies dependencies) {
this.dependencies = dependencies;
}

@ -43,12 +43,13 @@ import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.filter.DependencyFilterUtils;
import org.springframework.boot.cli.compiler.DependencyResolutionContext;
/**
* A {@link GrapeEngine} implementation that uses <a
* href="http://eclipse.org/aether">Aether</a>, the dependency resolution system used by
* Maven.
*
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
@ -58,7 +59,7 @@ public class AetherGrapeEngine implements GrapeEngine {
private static final Collection<Exclusion> WILDCARD_EXCLUSION = Arrays
.asList(new Exclusion("*", "*", "*", "*"));
private final List<Dependency> managedDependencies = new ArrayList<Dependency>();
private final DependencyResolutionContext resolutionContext;
private final ProgressReporter progressReporter;
@ -74,11 +75,11 @@ public class AetherGrapeEngine implements GrapeEngine {
RepositorySystem repositorySystem,
DefaultRepositorySystemSession repositorySystemSession,
List<RemoteRepository> remoteRepositories,
List<Dependency> managedDependencies) {
DependencyResolutionContext resolutionContext) {
this.classLoader = classLoader;
this.repositorySystem = repositorySystem;
this.session = repositorySystemSession;
this.managedDependencies.addAll(managedDependencies);
this.resolutionContext = resolutionContext;
this.repositories = new ArrayList<RemoteRepository>();
List<RemoteRepository> remotes = new ArrayList<RemoteRepository>(
@ -128,11 +129,13 @@ public class AetherGrapeEngine implements GrapeEngine {
@SuppressWarnings("unchecked")
private List<Exclusion> createExclusions(Map<?, ?> args) {
List<Exclusion> exclusions = new ArrayList<Exclusion>();
List<Map<String, Object>> exclusionMaps = (List<Map<String, Object>>) args
.get("excludes");
if (exclusionMaps != null) {
for (Map<String, Object> exclusionMap : exclusionMaps) {
exclusions.add(createExclusion(exclusionMap));
if (args != null) {
List<Map<String, Object>> exclusionMaps = (List<Map<String, Object>>) args
.get("excludes");
if (exclusionMaps != null) {
for (Map<String, Object> exclusionMap : exclusionMaps) {
exclusions.add(createExclusion(exclusionMap));
}
}
}
return exclusions;
@ -168,7 +171,13 @@ public class AetherGrapeEngine implements GrapeEngine {
String group = (String) dependencyMap.get("group");
String module = (String) dependencyMap.get("module");
String version = (String) dependencyMap.get("version");
return new DefaultArtifact(group, module, "jar", version);
String type = (String) dependencyMap.get("type");
if (type == null) {
type = "jar";
}
return new DefaultArtifact(group, module, type, version);
}
private boolean isTransitive(Map<?, ?> dependencyMap) {
@ -182,7 +191,8 @@ public class AetherGrapeEngine implements GrapeEngine {
try {
CollectRequest collectRequest = new CollectRequest((Dependency) null,
dependencies, new ArrayList<RemoteRepository>(this.repositories));
collectRequest.setManagedDependencies(this.managedDependencies);
collectRequest.setManagedDependencies(this.resolutionContext
.getManagedDependencies());
DependencyRequest dependencyRequest = new DependencyRequest(collectRequest,
DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE));
@ -190,7 +200,8 @@ public class AetherGrapeEngine implements GrapeEngine {
DependencyResult dependencyResult = this.repositorySystem
.resolveDependencies(this.session, dependencyRequest);
this.managedDependencies.addAll(getDependencies(dependencyResult));
this.resolutionContext.getManagedDependencies().addAll(
getDependencies(dependencyResult));
return getFiles(dependencyResult);
}
@ -252,13 +263,26 @@ public class AetherGrapeEngine implements GrapeEngine {
}
@Override
public URI[] resolve(Map args, Map... dependencies) {
throw new UnsupportedOperationException("Resolving to URIs is not supported");
public URI[] resolve(Map args, Map... dependencyMaps) {
return this.resolve(args, null, dependencyMaps);
}
@Override
public URI[] resolve(Map args, List depsInfo, Map... dependencies) {
throw new UnsupportedOperationException("Resolving to URIs is not supported");
public URI[] resolve(Map args, List depsInfo, Map... dependencyMaps) {
List<Exclusion> exclusions = createExclusions(args);
List<Dependency> dependencies = createDependencies(dependencyMaps, exclusions);
try {
List<File> files = resolve(dependencies);
List<URI> uris = new ArrayList<URI>(files.size());
for (File file : files) {
uris.add(file.toURI());
}
return uris.toArray(new URI[uris.size()]);
}
catch (Exception e) {
throw new DependencyResolutionFailedException(e);
}
}
@Override

@ -26,7 +26,6 @@ import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
import org.eclipse.aether.repository.RemoteRepository;
@ -36,16 +35,18 @@ import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.spi.locator.ServiceLocator;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.springframework.boot.cli.compiler.DependencyResolutionContext;
/**
* Utility class to create a pre-configured {@link AetherGrapeEngine}.
*
*
* @author Andy Wilkinson
*/
public abstract class AetherGrapeEngineFactory {
public static AetherGrapeEngine create(GroovyClassLoader classLoader,
List<RepositoryConfiguration> repositoryConfigurations) {
List<RepositoryConfiguration> repositoryConfigurations,
DependencyResolutionContext dependencyResolutionContext) {
RepositorySystem repositorySystem = createServiceLocator().getService(
RepositorySystem.class);
@ -63,12 +64,9 @@ public abstract class AetherGrapeEngineFactory {
new DefaultRepositorySystemSessionAutoConfiguration().apply(
repositorySystemSession, repositorySystem);
List<Dependency> managedDependencies = new ManagedDependenciesFactory()
.getManagedDependencies();
return new AetherGrapeEngine(classLoader, repositorySystem,
repositorySystemSession, createRepositories(repositoryConfigurations),
managedDependencies);
dependencyResolutionContext);
}
private static ServiceLocator createServiceLocator() {
@ -88,7 +86,7 @@ public abstract class AetherGrapeEngineFactory {
for (RepositoryConfiguration repositoryConfiguration : repositoryConfigurations) {
RemoteRepository.Builder builder = new RemoteRepository.Builder(
repositoryConfiguration.getName(), "default", repositoryConfiguration
.getUri().toASCIIString());
.getUri().toASCIIString());
if (!repositoryConfiguration.getSnapshotsEnabled()) {
builder.setSnapshotPolicy(new RepositoryPolicy(false,

@ -30,7 +30,7 @@ import org.springframework.boot.dependency.tools.VersionManagedDependencies;
/**
* Factory to create Maven {@link Dependency} objects from Boot
* {@link PomManagedDependencies}.
*
*
* @author Phillip Webb
*/
public class ManagedDependenciesFactory {
@ -41,7 +41,7 @@ public class ManagedDependenciesFactory {
this(new VersionManagedDependencies());
}
ManagedDependenciesFactory(ManagedDependencies dependencies) {
public ManagedDependenciesFactory(ManagedDependencies dependencies) {
this.dependencies = dependencies;
}

@ -0,0 +1,42 @@
/*
* Copyright 2012-2014 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.groovy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Used to provide an alternative source of dependency metadata that is used to deduce
* groups and versions when processing {@code @Grab} dependencies.
*
* @author Andy Wilkinson
* @since 1.1.0
*/
@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE,
ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE })
@Retention(RetentionPolicy.SOURCE)
public @interface GrabMetadata {
/**
* One or more sets of colon-separated coordinates ({@code group:module:version}) of a
* properties file that contains dependency metadata that will add to and override the
* default metadata.
*/
String[] value();
}

@ -17,6 +17,8 @@
package org.springframework.boot.cli;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import org.junit.After;
import org.junit.Before;
@ -26,10 +28,11 @@ import org.springframework.boot.cli.command.grab.GrabCommand;
import org.springframework.util.FileSystemUtils;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Integration tests for {@link GrabCommand}
*
*
* @author Andy Wilkinson
* @author Dave Syer
*/
@ -58,4 +61,32 @@ public class GrabCommandIntegrationTests {
// Should be resolved from local repository cache
assertTrue(output.contains("Downloading: file:"));
}
@Test
public void duplicateGrabMetadataAnnotationsProducesAnError() throws Exception {
try {
this.cli.grab("duplicateGrabMetadata.groovy");
fail();
}
catch (Exception e) {
assertTrue(e.getMessage().contains("Duplicate @GrabMetadata annotation"));
}
}
@Test
public void customMetadata() throws Exception {
System.setProperty("grape.root", "target");
File testArtifactDir = new File("target/repository/test/test/1.0.0");
testArtifactDir.mkdirs();
File testArtifact = new File(testArtifactDir, "test-1.0.0.properties");
testArtifact.createNewFile();
PrintWriter writer = new PrintWriter(new FileWriter(testArtifact));
writer.println("javax.ejb\\:ejb-api=3.0");
writer.close();
this.cli.grab("customGrabMetadata.groovy", "--autoconfigure=false");
assertTrue(new File("target/repository/javax/ejb/ejb-api/3.0").isDirectory());
}
}

@ -50,7 +50,7 @@ import static org.mockito.Mockito.when;
/**
* Tests for {@link ResolveDependencyCoordinatesTransformation}
*
*
* @author Andy Wilkinson
*/
public final class ResolveDependencyCoordinatesTransformationTests {
@ -64,9 +64,12 @@ public final class ResolveDependencyCoordinatesTransformationTests {
private final ArtifactCoordinatesResolver coordinatesResolver = mock(ArtifactCoordinatesResolver.class);
private final ASTTransformation transformation = new ResolveDependencyCoordinatesTransformation(
private final DependencyResolutionContext resolutionContext = new DependencyResolutionContext(
this.coordinatesResolver);
private final ASTTransformation transformation = new ResolveDependencyCoordinatesTransformation(
this.resolutionContext);
@Before
public void setupExpectations() {
when(this.coordinatesResolver.getGroupId("spring-core")).thenReturn(

@ -24,12 +24,13 @@ import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.boot.cli.compiler.DependencyResolutionContext;
import static org.junit.Assert.assertEquals;
/**
* Tests for {@link AetherGrapeEngine}.
*
*
* @author Andy Wilkinson
*/
public class AetherGrapeEngineTests {
@ -38,7 +39,8 @@ public class AetherGrapeEngineTests {
private final AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(
this.groovyClassLoader, Arrays.asList(new RepositoryConfiguration("central",
URI.create("http://repo1.maven.org/maven2"), false)));
URI.create("http://repo1.maven.org/maven2"), false)),
new DependencyResolutionContext());
@Test
public void dependencyResolution() {
@ -47,7 +49,7 @@ public class AetherGrapeEngineTests {
this.grapeEngine.grab(args,
createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"));
assertEquals(5, this.groovyClassLoader.getURLs().length);
assertEquals(6, this.groovyClassLoader.getURLs().length);
}
@SuppressWarnings("unchecked")
@ -61,7 +63,7 @@ public class AetherGrapeEngineTests {
createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"),
createDependency("org.springframework", "spring-beans", "3.2.4.RELEASE"));
assertEquals(3, this.groovyClassLoader.getURLs().length);
assertEquals(4, this.groovyClassLoader.getURLs().length);
}
@Test
@ -86,7 +88,7 @@ public class AetherGrapeEngineTests {
createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"));
assertEquals(0, this.groovyClassLoader.getURLs().length);
assertEquals(5, customClassLoader.getURLs().length);
assertEquals(6, customClassLoader.getURLs().length);
}
@Test

@ -0,0 +1,5 @@
@org.springframework.boot.groovy.GrabMetadata('test:test:1.0.0')
@Grab('ejb-api')
class CustomGrabMetadata {
}

@ -0,0 +1,5 @@
@GrabMetadata("foo:bar:1.0")
@GrabMetadata("alpha:bravo:2.0")
class DuplicateGrabMetadata {
}

@ -153,6 +153,31 @@ in the Spring Boot CLI source code to understand exactly how customizations are
[[cli-default-grab-deduced-coordinates]]
==== Deduced ``grab'' coordinates
Spring Boot extends Groovy's standard `@Grab` support by allowing you to specify a dependency
without a group or version, for example `@Grab('freemarker')`. This will consult Spring Boot's
default dependency metadata to deduce the artifact's group and version. Note that the default
metadata is tied to the version of the CLI that you're using it will only change when you move
to a new version of the CLI, putting you in control of when the versions of your dependencies
may change.
[[cli-default-grab-deduced-coordinates-custom-metadata]]
===== Custom ``grab'' metadata
Spring Boot provides a new annotation, `@GrabMetadata` that can be used to provide custom
dependency metadata that overrides Spring Boot's defaults. This metadata is specified by
using the new annotation to provide the coordinates of one or more properties files. For example `
@GrabMetadata(['com.example:versions-one:1.0.0', 'com.example.versions-two:1.0.0'])`. The
properties files are applied in the order that their specified. In the example above, this means
that properties in `versions-two` will override properties in `versions-one`. Each entry in
each properties file must be in the form `group:module=version`. You can use `@GrabVersions`
anywhere that you can use `@Grab`, however, to ensure consistent ordering of the metadata, you
can only use `@GrabVersions` at most once in your application.
[[cli-default-import-statements]]
==== Default import statements
To help reduce the size of your Groovy code, several `import` statements are

Loading…
Cancel
Save