Commit d2fc80b8 authored by Andy Wilkinson's avatar Andy Wilkinson

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
parent 3f498a48
/*
* 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,7 +23,6 @@ import org.codehaus.groovy.ast.ClassNode; ...@@ -23,7 +23,6 @@ import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation; import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
/** /**
* {@link ASTTransformation} to apply * {@link ASTTransformation} to apply
...@@ -38,15 +37,15 @@ public class DependencyAutoConfigurationTransformation implements ASTTransformat ...@@ -38,15 +37,15 @@ public class DependencyAutoConfigurationTransformation implements ASTTransformat
private final GroovyClassLoader loader; private final GroovyClassLoader loader;
private final ArtifactCoordinatesResolver coordinatesResolver; private final DependencyResolutionContext dependencyResolutionContext;
private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations; private final Iterable<CompilerAutoConfiguration> compilerAutoConfigurations;
public DependencyAutoConfigurationTransformation(GroovyClassLoader loader, public DependencyAutoConfigurationTransformation(GroovyClassLoader loader,
ArtifactCoordinatesResolver coordinatesResolver, DependencyResolutionContext dependencyResolutionContext,
Iterable<CompilerAutoConfiguration> compilerAutoConfigurations) { Iterable<CompilerAutoConfiguration> compilerAutoConfigurations) {
this.loader = loader; this.loader = loader;
this.coordinatesResolver = coordinatesResolver; this.dependencyResolutionContext = dependencyResolutionContext;
this.compilerAutoConfigurations = compilerAutoConfigurations; this.compilerAutoConfigurations = compilerAutoConfigurations;
} }
...@@ -62,7 +61,7 @@ public class DependencyAutoConfigurationTransformation implements ASTTransformat ...@@ -62,7 +61,7 @@ public class DependencyAutoConfigurationTransformation implements ASTTransformat
private void visitModule(ModuleNode module) { private void visitModule(ModuleNode module) {
DependencyCustomizer dependencies = new DependencyCustomizer(this.loader, module, DependencyCustomizer dependencies = new DependencyCustomizer(this.loader, module,
this.coordinatesResolver); this.dependencyResolutionContext);
for (ClassNode classNode : module.getClasses()) { for (ClassNode classNode : module.getClasses()) {
for (CompilerAutoConfiguration autoConfiguration : this.compilerAutoConfigurations) { for (CompilerAutoConfiguration autoConfiguration : this.compilerAutoConfigurations) {
if (autoConfiguration.matches(classNode)) { if (autoConfiguration.matches(classNode)) {
......
...@@ -42,17 +42,17 @@ public class DependencyCustomizer { ...@@ -42,17 +42,17 @@ public class DependencyCustomizer {
private final ClassNode classNode; private final ClassNode classNode;
private final ArtifactCoordinatesResolver coordinatesResolver; private final DependencyResolutionContext dependencyResolutionContext;
/** /**
* Create a new {@link DependencyCustomizer} instance. * Create a new {@link DependencyCustomizer} instance.
* @param loader * @param loader
*/ */
public DependencyCustomizer(GroovyClassLoader loader, ModuleNode moduleNode, public DependencyCustomizer(GroovyClassLoader loader, ModuleNode moduleNode,
ArtifactCoordinatesResolver coordinatesResolver) { DependencyResolutionContext dependencyResolutionContext) {
this.loader = loader; this.loader = loader;
this.classNode = moduleNode.getClasses().get(0); this.classNode = moduleNode.getClasses().get(0);
this.coordinatesResolver = coordinatesResolver; this.dependencyResolutionContext = dependencyResolutionContext;
} }
/** /**
...@@ -62,7 +62,7 @@ public class DependencyCustomizer { ...@@ -62,7 +62,7 @@ public class DependencyCustomizer {
protected DependencyCustomizer(DependencyCustomizer parent) { protected DependencyCustomizer(DependencyCustomizer parent) {
this.loader = parent.loader; this.loader = parent.loader;
this.classNode = parent.classNode; this.classNode = parent.classNode;
this.coordinatesResolver = parent.coordinatesResolver; this.dependencyResolutionContext = parent.dependencyResolutionContext;
} }
public String getVersion(String artifactId) { public String getVersion(String artifactId) {
...@@ -71,7 +71,8 @@ public class DependencyCustomizer { ...@@ -71,7 +71,8 @@ public class DependencyCustomizer {
} }
public String getVersion(String artifactId, String defaultVersion) { public String getVersion(String artifactId, String defaultVersion) {
String version = this.coordinatesResolver.getVersion(artifactId); String version = this.dependencyResolutionContext
.getArtifactCoordinatesResolver().getVersion(artifactId);
if (version == null) { if (version == null) {
version = defaultVersion; version = defaultVersion;
} }
...@@ -201,9 +202,11 @@ public class DependencyCustomizer { ...@@ -201,9 +202,11 @@ public class DependencyCustomizer {
*/ */
public DependencyCustomizer add(String module, boolean transitive) { public DependencyCustomizer add(String module, boolean transitive) {
if (canAdd()) { if (canAdd()) {
ArtifactCoordinatesResolver artifactCoordinatesResolver = this.dependencyResolutionContext
.getArtifactCoordinatesResolver();
this.classNode.addAnnotation(createGrabAnnotation( this.classNode.addAnnotation(createGrabAnnotation(
this.coordinatesResolver.getGroupId(module), module, artifactCoordinatesResolver.getGroupId(module), module,
this.coordinatesResolver.getVersion(module), transitive)); artifactCoordinatesResolver.getVersion(module), transitive));
} }
return this; return this;
} }
......
/*
* 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;
}
}
/*
* 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; ...@@ -43,8 +43,6 @@ import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.transform.ASTTransformation; import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.ASTTransformationVisitor; 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.AetherGrapeEngine;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngineFactory; import org.springframework.boot.cli.compiler.grape.AetherGrapeEngineFactory;
import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller; import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller;
...@@ -71,8 +69,6 @@ import org.springframework.boot.cli.util.ResourceUtils; ...@@ -71,8 +69,6 @@ import org.springframework.boot.cli.util.ResourceUtils;
*/ */
public class GroovyCompiler { public class GroovyCompiler {
private final ArtifactCoordinatesResolver coordinatesResolver;
private final GroovyCompilerConfiguration configuration; private final GroovyCompilerConfiguration configuration;
private final ExtendedGroovyClassLoader loader; private final ExtendedGroovyClassLoader loader;
...@@ -90,10 +86,10 @@ public class GroovyCompiler { ...@@ -90,10 +86,10 @@ public class GroovyCompiler {
this.configuration = configuration; this.configuration = configuration;
this.loader = createLoader(configuration); this.loader = createLoader(configuration);
this.coordinatesResolver = new ManagedDependenciesArtifactCoordinatesResolver(); DependencyResolutionContext resolutionContext = new DependencyResolutionContext();
AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader, AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader,
configuration.getRepositoryConfiguration()); configuration.getRepositoryConfiguration(), resolutionContext);
GrapeEngineInstaller.install(grapeEngine); GrapeEngineInstaller.install(grapeEngine);
...@@ -108,12 +104,13 @@ public class GroovyCompiler { ...@@ -108,12 +104,13 @@ public class GroovyCompiler {
} }
this.transformations = new ArrayList<ASTTransformation>(); this.transformations = new ArrayList<ASTTransformation>();
this.transformations.add(new GrabMetadataTransformation(resolutionContext));
this.transformations.add(new DependencyAutoConfigurationTransformation( this.transformations.add(new DependencyAutoConfigurationTransformation(
this.loader, this.coordinatesResolver, this.compilerAutoConfigurations)); this.loader, resolutionContext, this.compilerAutoConfigurations));
this.transformations.add(new GroovyBeansTransformation()); this.transformations.add(new GroovyBeansTransformation());
if (this.configuration.isGuessDependencies()) { if (this.configuration.isGuessDependencies()) {
this.transformations.add(new ResolveDependencyCoordinatesTransformation( this.transformations.add(new ResolveDependencyCoordinatesTransformation(
this.coordinatesResolver)); resolutionContext));
} }
} }
......
...@@ -21,21 +21,13 @@ import groovy.lang.Grab; ...@@ -21,21 +21,13 @@ import groovy.lang.Grab;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.List;
import java.util.Set; 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.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.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation; import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesResolver;
/** /**
* {@link ASTTransformation} to resolve {@link Grab} artifact coordinates. * {@link ASTTransformation} to resolve {@link Grab} artifact coordinates.
...@@ -43,61 +35,27 @@ import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesRes ...@@ -43,61 +35,27 @@ import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesRes
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
public class ResolveDependencyCoordinatesTransformation implements ASTTransformation { public class ResolveDependencyCoordinatesTransformation extends
AnnotatedNodeASTTransformation {
private static final Set<String> GRAB_ANNOTATION_NAMES = Collections private static final Set<String> GRAB_ANNOTATION_NAMES = Collections
.unmodifiableSet(new HashSet<String>(Arrays.asList(Grab.class.getName(), .unmodifiableSet(new HashSet<String>(Arrays.asList(Grab.class.getName(),
Grab.class.getSimpleName()))); Grab.class.getSimpleName())));
private final ArtifactCoordinatesResolver coordinatesResolver; private final DependencyResolutionContext resolutionContext;
public ResolveDependencyCoordinatesTransformation( public ResolveDependencyCoordinatesTransformation(
ArtifactCoordinatesResolver coordinatesResolver) { DependencyResolutionContext resolutionContext) {
this.coordinatesResolver = coordinatesResolver; super(GRAB_ANNOTATION_NAMES);
this.resolutionContext = resolutionContext;
} }
@Override @Override
public void visit(ASTNode[] nodes, SourceUnit source) { protected void processAnnotationNodes(List<AnnotationNode> annotationNodes) {
ClassVisitor classVisitor = new ClassVisitor(source); for (AnnotationNode annotationNode : annotationNodes) {
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); transformGrabAnnotation(annotationNode);
} }
} }
}
}
private void transformGrabAnnotation(AnnotationNode grabAnnotation) { private void transformGrabAnnotation(AnnotationNode grabAnnotation) {
grabAnnotation.setMember("initClass", new ConstantExpression(false)); grabAnnotation.setMember("initClass", new ConstantExpression(false));
...@@ -129,10 +87,12 @@ public class ResolveDependencyCoordinatesTransformation implements ASTTransforma ...@@ -129,10 +87,12 @@ public class ResolveDependencyCoordinatesTransformation implements ASTTransforma
module = (String) ((ConstantExpression) expression).getValue(); module = (String) ((ConstantExpression) expression).getValue();
} }
if (annotation.getMember("group") == null) { 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) { 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 ...@@ -140,24 +100,4 @@ public class ResolveDependencyCoordinatesTransformation implements ASTTransforma
ConstantExpression expression = new ConstantExpression(value); ConstantExpression expression = new ConstantExpression(value);
annotation.setMember(name, expression); 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);
}
}
} }
...@@ -63,7 +63,8 @@ public class SpringBootCompilerAutoConfiguration extends CompilerAutoConfigurati ...@@ -63,7 +63,8 @@ public class SpringBootCompilerAutoConfiguration extends CompilerAutoConfigurati
"org.springframework.core.annotation.Order", "org.springframework.core.annotation.Order",
"org.springframework.core.io.ResourceLoader", "org.springframework.core.io.ResourceLoader",
"org.springframework.boot.CommandLineRunner", "org.springframework.boot.CommandLineRunner",
"org.springframework.boot.autoconfigure.EnableAutoConfiguration"); "org.springframework.boot.autoconfigure.EnableAutoConfiguration",
"org.springframework.boot.groovy.GrabMetadata");
imports.addStarImports("org.springframework.stereotype", imports.addStarImports("org.springframework.stereotype",
"org.springframework.scheduling.annotation"); "org.springframework.scheduling.annotation");
} }
......
...@@ -34,7 +34,7 @@ public class ManagedDependenciesArtifactCoordinatesResolver implements ...@@ -34,7 +34,7 @@ public class ManagedDependenciesArtifactCoordinatesResolver implements
this(new VersionManagedDependencies()); this(new VersionManagedDependencies());
} }
ManagedDependenciesArtifactCoordinatesResolver(ManagedDependencies dependencies) { public ManagedDependenciesArtifactCoordinatesResolver(ManagedDependencies dependencies) {
this.dependencies = dependencies; this.dependencies = dependencies;
} }
......
...@@ -43,6 +43,7 @@ import org.eclipse.aether.resolution.DependencyRequest; ...@@ -43,6 +43,7 @@ import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResult; import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.artifact.JavaScopes; import org.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.filter.DependencyFilterUtils; import org.eclipse.aether.util.filter.DependencyFilterUtils;
import org.springframework.boot.cli.compiler.DependencyResolutionContext;
/** /**
* A {@link GrapeEngine} implementation that uses <a * A {@link GrapeEngine} implementation that uses <a
...@@ -58,7 +59,7 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -58,7 +59,7 @@ public class AetherGrapeEngine implements GrapeEngine {
private static final Collection<Exclusion> WILDCARD_EXCLUSION = Arrays private static final Collection<Exclusion> WILDCARD_EXCLUSION = Arrays
.asList(new Exclusion("*", "*", "*", "*")); .asList(new Exclusion("*", "*", "*", "*"));
private final List<Dependency> managedDependencies = new ArrayList<Dependency>(); private final DependencyResolutionContext resolutionContext;
private final ProgressReporter progressReporter; private final ProgressReporter progressReporter;
...@@ -74,11 +75,11 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -74,11 +75,11 @@ public class AetherGrapeEngine implements GrapeEngine {
RepositorySystem repositorySystem, RepositorySystem repositorySystem,
DefaultRepositorySystemSession repositorySystemSession, DefaultRepositorySystemSession repositorySystemSession,
List<RemoteRepository> remoteRepositories, List<RemoteRepository> remoteRepositories,
List<Dependency> managedDependencies) { DependencyResolutionContext resolutionContext) {
this.classLoader = classLoader; this.classLoader = classLoader;
this.repositorySystem = repositorySystem; this.repositorySystem = repositorySystem;
this.session = repositorySystemSession; this.session = repositorySystemSession;
this.managedDependencies.addAll(managedDependencies); this.resolutionContext = resolutionContext;
this.repositories = new ArrayList<RemoteRepository>(); this.repositories = new ArrayList<RemoteRepository>();
List<RemoteRepository> remotes = new ArrayList<RemoteRepository>( List<RemoteRepository> remotes = new ArrayList<RemoteRepository>(
...@@ -128,6 +129,7 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -128,6 +129,7 @@ public class AetherGrapeEngine implements GrapeEngine {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private List<Exclusion> createExclusions(Map<?, ?> args) { private List<Exclusion> createExclusions(Map<?, ?> args) {
List<Exclusion> exclusions = new ArrayList<Exclusion>(); List<Exclusion> exclusions = new ArrayList<Exclusion>();
if (args != null) {
List<Map<String, Object>> exclusionMaps = (List<Map<String, Object>>) args List<Map<String, Object>> exclusionMaps = (List<Map<String, Object>>) args
.get("excludes"); .get("excludes");
if (exclusionMaps != null) { if (exclusionMaps != null) {
...@@ -135,6 +137,7 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -135,6 +137,7 @@ public class AetherGrapeEngine implements GrapeEngine {
exclusions.add(createExclusion(exclusionMap)); exclusions.add(createExclusion(exclusionMap));
} }
} }
}
return exclusions; return exclusions;
} }
...@@ -168,7 +171,13 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -168,7 +171,13 @@ public class AetherGrapeEngine implements GrapeEngine {
String group = (String) dependencyMap.get("group"); String group = (String) dependencyMap.get("group");
String module = (String) dependencyMap.get("module"); String module = (String) dependencyMap.get("module");
String version = (String) dependencyMap.get("version"); 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) { private boolean isTransitive(Map<?, ?> dependencyMap) {
...@@ -182,7 +191,8 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -182,7 +191,8 @@ public class AetherGrapeEngine implements GrapeEngine {
try { try {
CollectRequest collectRequest = new CollectRequest((Dependency) null, CollectRequest collectRequest = new CollectRequest((Dependency) null,
dependencies, new ArrayList<RemoteRepository>(this.repositories)); dependencies, new ArrayList<RemoteRepository>(this.repositories));
collectRequest.setManagedDependencies(this.managedDependencies); collectRequest.setManagedDependencies(this.resolutionContext
.getManagedDependencies());
DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, DependencyRequest dependencyRequest = new DependencyRequest(collectRequest,
DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE)); DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE));
...@@ -190,7 +200,8 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -190,7 +200,8 @@ public class AetherGrapeEngine implements GrapeEngine {
DependencyResult dependencyResult = this.repositorySystem DependencyResult dependencyResult = this.repositorySystem
.resolveDependencies(this.session, dependencyRequest); .resolveDependencies(this.session, dependencyRequest);
this.managedDependencies.addAll(getDependencies(dependencyResult)); this.resolutionContext.getManagedDependencies().addAll(
getDependencies(dependencyResult));
return getFiles(dependencyResult); return getFiles(dependencyResult);
} }
...@@ -252,13 +263,26 @@ public class AetherGrapeEngine implements GrapeEngine { ...@@ -252,13 +263,26 @@ public class AetherGrapeEngine implements GrapeEngine {
} }
@Override @Override
public URI[] resolve(Map args, Map... dependencies) { public URI[] resolve(Map args, Map... dependencyMaps) {
throw new UnsupportedOperationException("Resolving to URIs is not supported"); return this.resolve(args, null, dependencyMaps);
} }
@Override @Override
public URI[] resolve(Map args, List depsInfo, Map... dependencies) { public URI[] resolve(Map args, List depsInfo, Map... dependencyMaps) {
throw new UnsupportedOperationException("Resolving to URIs is not supported"); 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 @Override
......
...@@ -26,7 +26,6 @@ import org.apache.maven.repository.internal.MavenRepositorySystemUtils; ...@@ -26,7 +26,6 @@ import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.impl.DefaultServiceLocator; import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem; import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RemoteRepository;
...@@ -36,6 +35,7 @@ import org.eclipse.aether.spi.connector.transport.TransporterFactory; ...@@ -36,6 +35,7 @@ import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.spi.locator.ServiceLocator; import org.eclipse.aether.spi.locator.ServiceLocator;
import org.eclipse.aether.transport.file.FileTransporterFactory; import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory; import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.springframework.boot.cli.compiler.DependencyResolutionContext;
/** /**
* Utility class to create a pre-configured {@link AetherGrapeEngine}. * Utility class to create a pre-configured {@link AetherGrapeEngine}.
...@@ -45,7 +45,8 @@ import org.eclipse.aether.transport.http.HttpTransporterFactory; ...@@ -45,7 +45,8 @@ import org.eclipse.aether.transport.http.HttpTransporterFactory;
public abstract class AetherGrapeEngineFactory { public abstract class AetherGrapeEngineFactory {
public static AetherGrapeEngine create(GroovyClassLoader classLoader, public static AetherGrapeEngine create(GroovyClassLoader classLoader,
List<RepositoryConfiguration> repositoryConfigurations) { List<RepositoryConfiguration> repositoryConfigurations,
DependencyResolutionContext dependencyResolutionContext) {
RepositorySystem repositorySystem = createServiceLocator().getService( RepositorySystem repositorySystem = createServiceLocator().getService(
RepositorySystem.class); RepositorySystem.class);
...@@ -63,12 +64,9 @@ public abstract class AetherGrapeEngineFactory { ...@@ -63,12 +64,9 @@ public abstract class AetherGrapeEngineFactory {
new DefaultRepositorySystemSessionAutoConfiguration().apply( new DefaultRepositorySystemSessionAutoConfiguration().apply(
repositorySystemSession, repositorySystem); repositorySystemSession, repositorySystem);
List<Dependency> managedDependencies = new ManagedDependenciesFactory()
.getManagedDependencies();
return new AetherGrapeEngine(classLoader, repositorySystem, return new AetherGrapeEngine(classLoader, repositorySystem,
repositorySystemSession, createRepositories(repositoryConfigurations), repositorySystemSession, createRepositories(repositoryConfigurations),
managedDependencies); dependencyResolutionContext);
} }
private static ServiceLocator createServiceLocator() { private static ServiceLocator createServiceLocator() {
......
...@@ -41,7 +41,7 @@ public class ManagedDependenciesFactory { ...@@ -41,7 +41,7 @@ public class ManagedDependenciesFactory {
this(new VersionManagedDependencies()); this(new VersionManagedDependencies());
} }
ManagedDependenciesFactory(ManagedDependencies dependencies) { public ManagedDependenciesFactory(ManagedDependencies dependencies) {
this.dependencies = dependencies; this.dependencies = dependencies;
} }
......
/*
* 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 @@ ...@@ -17,6 +17,8 @@
package org.springframework.boot.cli; package org.springframework.boot.cli;
import java.io.File; import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
...@@ -26,6 +28,7 @@ import org.springframework.boot.cli.command.grab.GrabCommand; ...@@ -26,6 +28,7 @@ import org.springframework.boot.cli.command.grab.GrabCommand;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/** /**
* Integration tests for {@link GrabCommand} * Integration tests for {@link GrabCommand}
...@@ -58,4 +61,32 @@ public class GrabCommandIntegrationTests { ...@@ -58,4 +61,32 @@ public class GrabCommandIntegrationTests {
// Should be resolved from local repository cache // Should be resolved from local repository cache
assertTrue(output.contains("Downloading: file:")); 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());
}
} }
...@@ -64,9 +64,12 @@ public final class ResolveDependencyCoordinatesTransformationTests { ...@@ -64,9 +64,12 @@ public final class ResolveDependencyCoordinatesTransformationTests {
private final ArtifactCoordinatesResolver coordinatesResolver = mock(ArtifactCoordinatesResolver.class); private final ArtifactCoordinatesResolver coordinatesResolver = mock(ArtifactCoordinatesResolver.class);
private final ASTTransformation transformation = new ResolveDependencyCoordinatesTransformation( private final DependencyResolutionContext resolutionContext = new DependencyResolutionContext(
this.coordinatesResolver); this.coordinatesResolver);
private final ASTTransformation transformation = new ResolveDependencyCoordinatesTransformation(
this.resolutionContext);
@Before @Before
public void setupExpectations() { public void setupExpectations() {
when(this.coordinatesResolver.getGroupId("spring-core")).thenReturn( when(this.coordinatesResolver.getGroupId("spring-core")).thenReturn(
......
...@@ -24,6 +24,7 @@ import java.util.HashMap; ...@@ -24,6 +24,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.cli.compiler.DependencyResolutionContext;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
...@@ -38,7 +39,8 @@ public class AetherGrapeEngineTests { ...@@ -38,7 +39,8 @@ public class AetherGrapeEngineTests {
private final AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create( private final AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(
this.groovyClassLoader, Arrays.asList(new RepositoryConfiguration("central", 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 @Test
public void dependencyResolution() { public void dependencyResolution() {
...@@ -47,7 +49,7 @@ public class AetherGrapeEngineTests { ...@@ -47,7 +49,7 @@ public class AetherGrapeEngineTests {
this.grapeEngine.grab(args, this.grapeEngine.grab(args,
createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE")); createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"));
assertEquals(5, this.groovyClassLoader.getURLs().length); assertEquals(6, this.groovyClassLoader.getURLs().length);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
...@@ -61,7 +63,7 @@ public class AetherGrapeEngineTests { ...@@ -61,7 +63,7 @@ public class AetherGrapeEngineTests {
createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"), createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"),
createDependency("org.springframework", "spring-beans", "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 @Test
...@@ -86,7 +88,7 @@ public class AetherGrapeEngineTests { ...@@ -86,7 +88,7 @@ public class AetherGrapeEngineTests {
createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE")); createDependency("org.springframework", "spring-jdbc", "3.2.4.RELEASE"));
assertEquals(0, this.groovyClassLoader.getURLs().length); assertEquals(0, this.groovyClassLoader.getURLs().length);
assertEquals(5, customClassLoader.getURLs().length); assertEquals(6, customClassLoader.getURLs().length);
} }
@Test @Test
......
@org.springframework.boot.groovy.GrabMetadata('test:test:1.0.0')
@Grab('ejb-api')
class CustomGrabMetadata {
}
\ No newline at end of file
@GrabMetadata("foo:bar:1.0")
@GrabMetadata("alpha:bravo:2.0")
class DuplicateGrabMetadata {
}
\ No newline at end of file
...@@ -153,6 +153,31 @@ in the Spring Boot CLI source code to understand exactly how customizations are ...@@ -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]] [[cli-default-import-statements]]
==== Default import statements ==== Default import statements
To help reduce the size of your Groovy code, several `import` statements are To help reduce the size of your Groovy code, several `import` statements are
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment