diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/AnnotatedNodeASTTransformation.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/AnnotatedNodeASTTransformation.java new file mode 100644 index 0000000000..37a8a2e35e --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/AnnotatedNodeASTTransformation.java @@ -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 interestingAnnotationNames; + + private List annotationNodes = new ArrayList(); + + private SourceUnit sourceUnit; + + protected AnnotatedNodeASTTransformation(Set 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 entry : module.getStaticImports() + .entrySet()) { + visitAnnotatedNode(entry.getValue()); + } + for (Map.Entry 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 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); + } + + } +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyAutoConfigurationTransformation.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyAutoConfigurationTransformation.java index bf0f56d78b..0be0e86ef3 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyAutoConfigurationTransformation.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyAutoConfigurationTransformation.java @@ -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 compilerAutoConfigurations; public DependencyAutoConfigurationTransformation(GroovyClassLoader loader, - ArtifactCoordinatesResolver coordinatesResolver, + DependencyResolutionContext dependencyResolutionContext, Iterable 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)) { diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyCustomizer.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyCustomizer.java index c2f4b06e90..401d0a1ff8 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyCustomizer.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyCustomizer.java @@ -32,7 +32,7 @@ import org.springframework.boot.cli.compiler.dependencies.ArtifactCoordinatesRes *

* 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; } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyResolutionContext.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyResolutionContext.java new file mode 100644 index 0000000000..aa6b495021 --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/DependencyResolutionContext.java @@ -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 managedDependencies = new ArrayList(); + + public DependencyResolutionContext() { + this(new ManagedDependenciesArtifactCoordinatesResolver()); + } + + DependencyResolutionContext(ArtifactCoordinatesResolver artifactCoordinatesResolver) { + this.artifactCoordinatesResolver = artifactCoordinatesResolver; + } + + void setManagedDependencies(ManagedDependencies managedDependencies) { + this.artifactCoordinatesResolver = new ManagedDependenciesArtifactCoordinatesResolver( + managedDependencies); + this.managedDependencies = new ArrayList( + new ManagedDependenciesFactory(managedDependencies) + .getManagedDependencies()); + + } + + ArtifactCoordinatesResolver getArtifactCoordinatesResolver() { + return this.artifactCoordinatesResolver; + } + + public List getManagedDependencies() { + return this.managedDependencies; + } +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GrabMetadataTransformation.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GrabMetadataTransformation.java new file mode 100644 index 0000000000..d7bf66b2a6 --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GrabMetadataTransformation.java @@ -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 GRAB_METADATA_ANNOTATION_NAMES = Collections + .unmodifiableSet(new HashSet(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 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> metadataDependencies = createDependencyMaps(valueExpression); + updateArtifactCoordinatesResolver(metadataDependencies); + } + + private List> createDependencyMaps(Expression valueExpression) { + Map dependency = null; + + List constantExpressions = new ArrayList(); + + 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> dependencies = new ArrayList>( + 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(); + 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> metadataDependencies) { + URI[] uris = Grape.getInstance().resolve(null, + metadataDependencies.toArray(new Map[metadataDependencies.size()])); + List managedDependencies = new ArrayList( + 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()); + } +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java index 44db3e943d..2be52d1f42 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java @@ -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; *

  • {@link CompilerAutoConfiguration} strategies will be read from * META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration * (per the standard java {@link ServiceLoader} contract) and applied during compilation
  • - * + * *
  • Multiple classes can be returned if the Groovy source defines more than one Class
  • - * + * *
  • Generated class files can also be loaded using * {@link ClassLoader#getResource(String)}
  • * - * + * * @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(); + 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> classes = new ArrayList>(); @@ -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); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformation.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformation.java index 7b787009f5..10046e63d5 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformation.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformation.java @@ -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 GRAB_ANNOTATION_NAMES = Collections .unmodifiableSet(new HashSet(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 entry : module.getStaticImports() - .entrySet()) { - visitAnnotatedNode(entry.getValue()); - } - for (Map.Entry 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 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); - } - - } } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringBootCompilerAutoConfiguration.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringBootCompilerAutoConfiguration.java index d7d9510620..963a59a466 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringBootCompilerAutoConfiguration.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringBootCompilerAutoConfiguration.java @@ -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"); } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/dependencies/ManagedDependenciesArtifactCoordinatesResolver.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/dependencies/ManagedDependenciesArtifactCoordinatesResolver.java index 5b3243222a..d4dc88ae63 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/dependencies/ManagedDependenciesArtifactCoordinatesResolver.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/dependencies/ManagedDependenciesArtifactCoordinatesResolver.java @@ -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; } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java index 567207f9e9..68d3dadab7 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java @@ -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 Aether, 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 WILDCARD_EXCLUSION = Arrays .asList(new Exclusion("*", "*", "*", "*")); - private final List managedDependencies = new ArrayList(); + private final DependencyResolutionContext resolutionContext; private final ProgressReporter progressReporter; @@ -74,11 +75,11 @@ public class AetherGrapeEngine implements GrapeEngine { RepositorySystem repositorySystem, DefaultRepositorySystemSession repositorySystemSession, List remoteRepositories, - List managedDependencies) { + DependencyResolutionContext resolutionContext) { this.classLoader = classLoader; this.repositorySystem = repositorySystem; this.session = repositorySystemSession; - this.managedDependencies.addAll(managedDependencies); + this.resolutionContext = resolutionContext; this.repositories = new ArrayList(); List remotes = new ArrayList( @@ -128,11 +129,13 @@ public class AetherGrapeEngine implements GrapeEngine { @SuppressWarnings("unchecked") private List createExclusions(Map args) { List exclusions = new ArrayList(); - List> exclusionMaps = (List>) args - .get("excludes"); - if (exclusionMaps != null) { - for (Map exclusionMap : exclusionMaps) { - exclusions.add(createExclusion(exclusionMap)); + if (args != null) { + List> exclusionMaps = (List>) args + .get("excludes"); + if (exclusionMaps != null) { + for (Map 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(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 exclusions = createExclusions(args); + List dependencies = createDependencies(dependencyMaps, exclusions); + + try { + List files = resolve(dependencies); + List uris = new ArrayList(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 diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java index 278f93a71d..9f5029ae39 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java @@ -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 repositoryConfigurations) { + List repositoryConfigurations, + DependencyResolutionContext dependencyResolutionContext) { RepositorySystem repositorySystem = createServiceLocator().getService( RepositorySystem.class); @@ -63,12 +64,9 @@ public abstract class AetherGrapeEngineFactory { new DefaultRepositorySystemSessionAutoConfiguration().apply( repositorySystemSession, repositorySystem); - List 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, diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/ManagedDependenciesFactory.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/ManagedDependenciesFactory.java index e7c9c41a88..b09642977d 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/ManagedDependenciesFactory.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/ManagedDependenciesFactory.java @@ -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; } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/groovy/GrabMetadata.java b/spring-boot-cli/src/main/java/org/springframework/boot/groovy/GrabMetadata.java new file mode 100644 index 0000000000..7427e42277 --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/groovy/GrabMetadata.java @@ -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(); +} diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/GrabCommandIntegrationTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/GrabCommandIntegrationTests.java index 2918fff2ee..e73f4cdc3b 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/GrabCommandIntegrationTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/GrabCommandIntegrationTests.java @@ -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()); + } } diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformationTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformationTests.java index d2cd26791c..cd349e2544 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformationTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/ResolveDependencyCoordinatesTransformationTests.java @@ -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( diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java index 9e81108131..537175befd 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java @@ -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 diff --git a/spring-boot-cli/src/test/resources/grab-samples/customGrabMetadata.groovy b/spring-boot-cli/src/test/resources/grab-samples/customGrabMetadata.groovy new file mode 100644 index 0000000000..2b22c50c99 --- /dev/null +++ b/spring-boot-cli/src/test/resources/grab-samples/customGrabMetadata.groovy @@ -0,0 +1,5 @@ +@org.springframework.boot.groovy.GrabMetadata('test:test:1.0.0') +@Grab('ejb-api') +class CustomGrabMetadata { + +} \ No newline at end of file diff --git a/spring-boot-cli/src/test/resources/grab-samples/duplicateGrabMetadata.groovy b/spring-boot-cli/src/test/resources/grab-samples/duplicateGrabMetadata.groovy new file mode 100644 index 0000000000..cc4a7d6e73 --- /dev/null +++ b/spring-boot-cli/src/test/resources/grab-samples/duplicateGrabMetadata.groovy @@ -0,0 +1,5 @@ +@GrabMetadata("foo:bar:1.0") +@GrabMetadata("alpha:bravo:2.0") +class DuplicateGrabMetadata { + +} \ No newline at end of file diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc index 94e46d1f29..644bfe6338 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc @@ -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