Support @Grab without a version or group

Usually, use of @Grab requires you to specify a group, module, and
version when identifying a dependency. This can be done in two
different ways:

@Grab(group='alpha', module='bravo', version='1.0.0')
@Grab('alpha:bravo:1.0.0')

This commit allows users to only specify a module: the group is
inferred and the version is the one dictated by the boot CLI. Both
forms are supported:

@Grab(module='bravo')
@Grab('bravo')

Groovy's global AST transformations, which is how Grab is implemented,
do not support ordering and we need to augment the AST for the Grab
annotation before its processed by the Grab AST transformation. To
work around this, reflection is used to get hold of the compile
operations in the conversion phase, and a new AST transformation is
inserted immediately before the first AST transformation operation.

To allow a module's groupId and version to be resolved consistently,
META-INF/springcli.properties has been enhanced to include properties
for each module that we want to support in the following form:

<module>.groudId = <groudId>
<module>.version = <version>

<groupId> and <version> are taken from the Maven project's
dependencies and VPP, a Velocity-based pre-processor, is used to
automatically generate the enhanced properties file.

To prevent pollution of spring-boot-cli's class path with the
dependencies that are only required to populate springcli.properties,
a separate project, spring-boot-cli-properties, has been created.
spring-boot-cli depends upon this now project causing it to, via the
shade plug, include the properties file in its jar.

Previously DependencyCustomizer allow a dependency to be added by
specifying its full coordinates, i.e. a group ID, artifact ID, and
version. This commit updates DependencyCustomizer to only require
an artifact/module ID. The group ID and version are then resolved
using the same mechanism as the enhanced @Grab support.

[#56328644] [bs-312] Allow @Grab without version
pull/76/merge
Andy Wilkinson 11 years ago
parent 4ddae32de9
commit c1ec5e5c0e

@ -37,6 +37,7 @@
<module>spring-boot-actuator</module>
<module>spring-boot-starters</module>
<module>spring-boot-cli</module>
<module>spring-boot-cli-properties</module>
<module>spring-boot-integration-tests</module>
</modules>
</profile>

@ -0,0 +1,531 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>0.5.0.BUILD-SNAPSHOT</version>
<relativePath>../spring-boot-parent</relativePath>
</parent>
<artifactId>spring-boot-cli-properties</artifactId>
<packaging>jar</packaging>
<properties>
<main.basedir>${basedir}/..</main.basedir>
</properties>
<dependencies>
<!-- Provided (to populate springcli.properties) -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-logging-juli</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jsp-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-templates</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>javax.servlet</artifactId>
<groupId>org.eclipse.jetty.orbit</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jsp</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectreactor</groupId>
<artifactId>reactor-spring</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>quartz</groupId>
<artifactId>quartz</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-http</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-ip</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-dsl-groovy-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring3</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity3</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jms_1.1_spec</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${project.build.directory}/generated-resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>generate-cli-properties</id>
<phase>generate-sources</phase>
<configuration>
<target>
<typedef resource="foundrylogic/vpp/typedef.properties" />
<taskdef resource="foundrylogic/vpp/taskdef.properties" />
<property name="dependencies" value="${project.parent}"/>
<vppcopy file="${basedir}/src/main/resources/META-INF/springcli.properties.vpp"
tofile="${project.build.directory}/generated-resources/META-INF/springcli.properties"
overwrite="true"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,18 @@
#set( $artifacts = $project.getReference('maven.project').artifacts )
#foreach( $artifact in $artifacts )
#if ( $artifact.scope == 'provided' )
${artifact.artifactId}.version: $artifact.version
${artifact.artifactId}.groupId: $artifact.groupId
#end
#end
groovy.version: $ant.get('groovy.version')
jetty.version: $ant.get('jetty.version')
reactor.version: $ant.get('reactor.version')
spring.version: $ant.get('spring.version')
spring-batch.version: $ant.get('spring-batch.version')
spring-boot.version: $ant.get('project.version')
spring-rabbit.version: $ant.get('spring-rabbit.version')
spring-security.version: $ant.get('spring-security.version')
spring-integration.version: $ant.get('spring-integration.version')
spring-integration-groovydsl.version: $ant.get('spring-integration-groovydsl.version')
tomcat.version: $ant.get('tomcat.version')

@ -16,6 +16,11 @@
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-cli-properties</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jopt-simple</groupId>
<artifactId>jopt-simple</artifactId>
@ -62,12 +67,6 @@
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>

@ -0,0 +1,24 @@
package org.test
@Grab('spring-boot-starter-web')
@Component
class Example implements CommandLineRunner {
@Autowired
private MyService myService
void run(String... args) {
print "Hello " + this.myService.sayWorld()
}
}
@Service
class MyService {
String sayWorld() {
return "World!"
}
}

@ -0,0 +1,46 @@
/*
* Copyright 2012-2013 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;
/**
* A resolver for artifacts' Maven coordinates, allowing a group id or version to be
* obtained from an artifact ID.
*
* @author Andy Wilkinson
*/
public interface ArtifactCoordinatesResolver {
/**
* Gets the group id of the artifact identified by the given {@code artifactId}.
* Returns {@code null} if the artifact is unknown to the resolver.
*
* @param artifactId The id of the artifact
*
* @return The group id of the artifact
*/
String getGroupId(String artifactId);
/**
* Gets the version of the artifact identified by the given {@code artifactId}.
* Returns {@code null} if the artifact is unknown to the resolver.
*
* @param artifactId The id of the artifact
*
* @return The version of the artifact
*/
String getVersion(String artifactId);
}

@ -20,14 +20,11 @@ import groovy.grape.Grape;
import groovy.lang.Grapes;
import groovy.lang.GroovyClassLoader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Customizer that allows dependencies to be added during compilation. Delegates to Groovy
@ -43,15 +40,17 @@ public class DependencyCustomizer {
private final List<Map<String, Object>> dependencies;
private Properties properties;
private final ArtifactCoordinatesResolver artifactCoordinatesResolver;
/**
* Create a new {@link DependencyCustomizer} instance. The {@link #call()} method must
* be used to actually resolve dependencies.
* @param loader
*/
public DependencyCustomizer(GroovyClassLoader loader) {
public DependencyCustomizer(GroovyClassLoader loader,
ArtifactCoordinatesResolver artifactCoordinatesResolver) {
this.loader = loader;
this.artifactCoordinatesResolver = artifactCoordinatesResolver;
this.dependencies = new ArrayList<Map<String, Object>>();
}
@ -61,27 +60,21 @@ public class DependencyCustomizer {
*/
protected DependencyCustomizer(DependencyCustomizer parent) {
this.loader = parent.loader;
this.artifactCoordinatesResolver = parent.artifactCoordinatesResolver;
this.dependencies = parent.dependencies;
}
public String getProperty(String key) {
return getProperty(key, "");
public String getVersion(String artifactId) {
return getVersion(artifactId, "");
}
public String getProperty(String key, String defaultValue) {
if (this.properties == null) {
this.properties = new Properties();
try {
for (URL url : Collections.list(this.loader
.getResources("META-INF/springcli.properties"))) {
this.properties.load(url.openStream());
}
}
catch (Exception e) {
// swallow and continue
}
public String getVersion(String artifactId, String defaultVersion) {
String version = this.artifactCoordinatesResolver.getVersion(artifactId);
if (version == null) {
version = defaultVersion;
}
return this.properties.getProperty(key, defaultValue);
return version;
}
/**
@ -216,25 +209,33 @@ public class DependencyCustomizer {
}
/**
* Add a single dependencies.
* @param group the group ID
* @param module the module ID
* @param version the version
* Add a single dependency and all of its dependencies. The group ID and version of
* the dependency are resolves using the customizer's
* {@link ArtifactCoordinatesResolver}.
* @param module The module ID
* @return this {@link DependencyCustomizer} for continued use
*/
public DependencyCustomizer add(String group, String module, String version) {
return this.add(group, module, version, true);
public DependencyCustomizer add(String module) {
return this.add(this.artifactCoordinatesResolver.getGroupId(module), module,
this.artifactCoordinatesResolver.getVersion(module), true);
}
/**
* Add a single dependencies.
* @param group the group ID
* @param module the module ID
* @param version the version
* Add a single dependency and, optionally, all of its dependencies. The group ID and
* version of the dependency are resolves using the customizer's
* {@link ArtifactCoordinatesResolver}.
* @param module The module ID
* @param transitive {@code true} if the transitive dependencies should also be added,
* otherwise {@code false}.
* @return this {@link DependencyCustomizer} for continued use
*/
public DependencyCustomizer add(String module, boolean transitive) {
return this.add(this.artifactCoordinatesResolver.getGroupId(module), module,
this.artifactCoordinatesResolver.getVersion(module), transitive);
}
@SuppressWarnings("unchecked")
public DependencyCustomizer add(String group, String module, String version,
private DependencyCustomizer add(String group, String module, String version,
boolean transitive) {
if (canAdd()) {
Map<String, Object> dependency = new HashMap<String, Object>();

@ -17,17 +17,25 @@
package org.springframework.boot.cli.compiler;
import groovy.grape.Grape;
import groovy.lang.Grab;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyClassLoader.ClassCollector;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
@ -37,6 +45,8 @@ import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.SourceUnit;
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;
/**
* Compiler for Groovy source files. Primarily a simple Facade for
@ -61,6 +71,8 @@ public class GroovyCompiler {
private ExtendedGroovyClassLoader loader;
private ArtifactCoordinatesResolver artifactCoordinatesResolver;
/**
* Create a new {@link GroovyCompiler} instance.
* @param configuration the compiler configuration
@ -73,9 +85,12 @@ public class GroovyCompiler {
if (configuration.getClasspath().length() > 0) {
this.loader.addClasspath(configuration.getClasspath());
}
this.artifactCoordinatesResolver = new PropertiesArtifactCoordinatesResolver(
this.loader);
new GrapeEngineCustomizer(Grape.getInstance()).customize();
compilerConfiguration
.addCompilationCustomizers(new CompilerAutoConfigureCustomizer());
}
public void addCompilationCustomizers(CompilationCustomizer... customizers) {
@ -115,8 +130,8 @@ public class GroovyCompiler {
CompilerConfiguration compilerConfiguration = this.loader.getConfiguration();
CompilationUnit compilationUnit = new CompilationUnit(compilerConfiguration,
null, this.loader);
final CompilationUnit compilationUnit = new CompilationUnit(
compilerConfiguration, null, this.loader);
SourceUnit sourceUnit = new SourceUnit(file[0], compilerConfiguration,
this.loader, compilationUnit.getErrorCollector());
ClassCollector collector = this.loader.createCollector(compilationUnit,
@ -124,6 +139,9 @@ public class GroovyCompiler {
compilationUnit.setClassgenCallback(collector);
compilationUnit.addSources(file);
addAstTransformations(compilationUnit);
compilationUnit.compile(Phases.CLASS_GENERATION);
for (Object loadedClass : collector.getLoadedClasses()) {
classes.add((Class<?>) loadedClass);
@ -145,6 +163,43 @@ public class GroovyCompiler {
}
@SuppressWarnings("rawtypes")
private void addAstTransformations(final CompilationUnit compilationUnit) {
try {
Field field = CompilationUnit.class.getDeclaredField("phaseOperations");
field.setAccessible(true);
LinkedList[] phaseOperations = (LinkedList[]) field.get(compilationUnit);
processConversionOperations(phaseOperations[Phases.CONVERSION]);
}
catch (Exception npe) {
throw new IllegalStateException(
"Phase operations not available from compilation unit");
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void processConversionOperations(LinkedList conversionOperations) {
for (int i = 0; i < conversionOperations.size(); i++) {
Object operation = conversionOperations.get(i);
if (operation.getClass().getName()
.startsWith(ASTTransformationVisitor.class.getName())) {
conversionOperations.add(i, new CompilationUnit.SourceUnitOperation() {
private final ASTTransformation transformation = new DefaultDependencyCoordinatesAstTransformation();
@Override
public void call(SourceUnit source) throws CompilationFailedException {
this.transformation.visit(new ASTNode[] { source.getAST() },
source);
}
});
break;
}
}
}
/**
* {@link CompilationCustomizer} to call {@link CompilerAutoConfiguration}s.
*/
@ -166,7 +221,8 @@ public class GroovyCompiler {
// Early sweep to get dependencies
DependencyCustomizer dependencyCustomizer = new DependencyCustomizer(
GroovyCompiler.this.loader);
GroovyCompiler.this.loader,
GroovyCompiler.this.artifactCoordinatesResolver);
for (CompilerAutoConfiguration autoConfiguration : customizers) {
if (autoConfiguration.matches(classNode)) {
if (GroovyCompiler.this.configuration.isGuessDependencies()) {
@ -200,4 +256,70 @@ public class GroovyCompiler {
}
private class DefaultDependencyCoordinatesAstTransformation implements
ASTTransformation {
@Override
public void visit(ASTNode[] nodes, final SourceUnit source) {
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
ModuleNode module = (ModuleNode) node;
for (ClassNode classNode : module.getClasses()) {
for (AnnotationNode annotationNode : classNode.getAnnotations()) {
if (isGrabAnnotation(annotationNode)) {
transformGrabAnnotationIfNecessary(annotationNode);
}
}
}
}
}
}
private boolean isGrabAnnotation(AnnotationNode annotationNode) {
String annotationClassName = annotationNode.getClassNode().getName();
return annotationClassName.equals(Grab.class.getName())
|| annotationClassName.equals(Grab.class.getSimpleName());
}
private void transformGrabAnnotationIfNecessary(AnnotationNode grabAnnotation) {
Expression valueExpression = grabAnnotation.getMember("value");
if (valueExpression instanceof ConstantExpression) {
ConstantExpression constantExpression = (ConstantExpression) valueExpression;
Object valueObject = constantExpression.getValue();
if (valueObject instanceof String) {
String value = (String) valueObject;
if (!isConvenienceForm(value)) {
transformGrabAnnotation(grabAnnotation, constantExpression);
}
}
}
}
private boolean isConvenienceForm(String value) {
return value.contains(":") || value.contains("#");
}
private void transformGrabAnnotation(AnnotationNode grabAnnotation,
ConstantExpression moduleExpression) {
grabAnnotation.setMember("module", moduleExpression);
String module = (String) moduleExpression.getValue();
if (grabAnnotation.getMember("group") == null) {
ConstantExpression groupIdExpression = new ConstantExpression(
GroovyCompiler.this.artifactCoordinatesResolver
.getGroupId(module));
grabAnnotation.setMember("group", groupIdExpression);
}
if (grabAnnotation.getMember("version") == null) {
ConstantExpression versionExpression = new ConstantExpression(
GroovyCompiler.this.artifactCoordinatesResolver
.getVersion(module));
grabAnnotation.setMember("version", versionExpression);
}
}
}
}

@ -0,0 +1,77 @@
/*
* Copyright 2012-2013 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.lang.GroovyClassLoader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Properties;
final class PropertiesArtifactCoordinatesResolver implements ArtifactCoordinatesResolver {
private final GroovyClassLoader loader;
private Properties properties = null;
public PropertiesArtifactCoordinatesResolver(GroovyClassLoader loader) {
this.loader = loader;
}
@Override
public String getGroupId(String artifactId) {
return getProperty(artifactId + ".groupId");
}
@Override
public String getVersion(String artifactId) {
return getProperty(artifactId + ".version");
}
private String getProperty(String name) {
if (this.properties == null) {
loadProperties();
}
return this.properties.getProperty(name);
}
private void loadProperties() {
Properties properties = new Properties();
try {
for (URL url : Collections.list(this.loader
.getResources("META-INF/springcli.properties"))) {
InputStream inputStream = url.openStream();
try {
properties.load(inputStream);
}
catch (IOException ioe) {
// Swallow and continue
}
finally {
inputStream.close();
}
}
}
catch (IOException e) {
// Swallow and continue
}
this.properties = properties;
}
}

@ -38,8 +38,7 @@ public class JdbcCompilerAutoConfiguration extends CompilerAutoConfiguration {
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies.ifAnyMissingClasses("org.springframework.jdbc.core.JdbcTemplate")
.add("org.springframework.boot", "spring-boot-starter-jdbc",
dependencies.getProperty("spring-boot.version"));
.add("spring-boot-starter-jdbc");
}
@Override

@ -16,6 +16,12 @@
package org.springframework.boot.cli.compiler.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
@ -23,8 +29,6 @@ import org.springframework.boot.cli.compiler.AstUtils;
import org.springframework.boot.cli.compiler.CompilerAutoConfiguration;
import org.springframework.boot.cli.compiler.DependencyCustomizer;
import java.lang.annotation.*;
/**
* {@link CompilerAutoConfiguration} for Spring JMS.
*
@ -34,17 +38,15 @@ public class JmsCompilerAutoConfiguration extends CompilerAutoConfiguration {
@Override
public boolean matches(ClassNode classNode) {
// Slightly weird detection algorithm because there is no @Enable annotation for
// Spring JMS
return AstUtils.hasAtLeastOneAnnotation(classNode, "EnableJmsMessaging");
// Slightly weird detection algorithm because there is no @Enable annotation for
// Spring JMS
return AstUtils.hasAtLeastOneAnnotation(classNode, "EnableJmsMessaging");
}
@Override
public void applyDependencies(DependencyCustomizer dependencies)
throws CompilationFailedException {
dependencies.add("org.springframework", "spring-jms",
dependencies.getProperty("spring.version")).add(
"org.apache.geronimo.specs", "geronimo-jms_1.1_spec", "1.1");
dependencies.add("spring-jms").add("geronimo-jms_1.1_spec");
}
@ -52,15 +54,15 @@ public class JmsCompilerAutoConfiguration extends CompilerAutoConfiguration {
public void applyImports(ImportCustomizer imports) throws CompilationFailedException {
imports.addStarImports("javax.jms", "org.springframework.jms.core",
"org.springframework.jms.listener",
"org.springframework.jms.listener.adapter")
.addImports(EnableJmsMessaging.class.getCanonicalName());
"org.springframework.jms.listener.adapter").addImports(
EnableJmsMessaging.class.getCanonicalName());
}
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public static @interface EnableJmsMessaging {
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public static @interface EnableJmsMessaging {
}
}
}

@ -16,6 +16,12 @@
package org.springframework.boot.cli.compiler.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
@ -23,45 +29,42 @@ import org.springframework.boot.cli.compiler.AstUtils;
import org.springframework.boot.cli.compiler.CompilerAutoConfiguration;
import org.springframework.boot.cli.compiler.DependencyCustomizer;
import java.lang.annotation.*;
/**
* {@link CompilerAutoConfiguration} for Spring Rabbit.
*
*
* @author Greg Turnquist
*/
public class RabbitCompilerAutoConfiguration extends CompilerAutoConfiguration {
@Override
public boolean matches(ClassNode classNode) {
// Slightly weird detection algorithm because there is no @Enable annotation for
// Integration
return AstUtils.hasAtLeastOneAnnotation(classNode, "EnableRabbitMessaging");
}
@Override
public void applyDependencies(DependencyCustomizer dependencies)
throws CompilationFailedException {
dependencies.add("org.springframework.amqp", "spring-rabbit",
dependencies.getProperty("spring-rabbit.version"));
@Override
public boolean matches(ClassNode classNode) {
// Slightly weird detection algorithm because there is no @Enable annotation for
// Integration
return AstUtils.hasAtLeastOneAnnotation(classNode, "EnableRabbitMessaging");
}
}
@Override
public void applyDependencies(DependencyCustomizer dependencies)
throws CompilationFailedException {
dependencies.add("spring-rabbit");
@Override
public void applyImports(ImportCustomizer imports) throws CompilationFailedException {
imports.addStarImports("org.springframework.amqp.rabbit.core",
"org.springframework.amqp.rabbit.connection",
"org.springframework.amqp.rabbit.listener",
"org.springframework.amqp.rabbit.listener.adapter",
"org.springframework.amqp.core").addImports(EnableRabbitMessaging.class.getCanonicalName());
}
}
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public static @interface EnableRabbitMessaging {
@Override
public void applyImports(ImportCustomizer imports) throws CompilationFailedException {
imports.addStarImports("org.springframework.amqp.rabbit.core",
"org.springframework.amqp.rabbit.connection",
"org.springframework.amqp.rabbit.listener",
"org.springframework.amqp.rabbit.listener.adapter",
"org.springframework.amqp.core").addImports(
EnableRabbitMessaging.class.getCanonicalName());
}
}
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public static @interface EnableRabbitMessaging {
}
}

@ -36,12 +36,8 @@ public class ReactorCompilerAutoConfiguration extends CompilerAutoConfiguration
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies
.ifAnyMissingClasses("org.reactor.Reactor")
.add("org.projectreactor", "reactor-spring",
dependencies.getProperty("reactor.version"), false)
.add("org.projectreactor", "reactor-core",
dependencies.getProperty("reactor.version"));
dependencies.ifAnyMissingClasses("reactor.core.Reactor")
.add("reactor-spring", false).add("reactor-core");
}
@Override

@ -38,11 +38,9 @@ public class SpringBatchCompilerAutoConfiguration extends CompilerAutoConfigurat
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies.ifAnyMissingClasses("org.springframework.batch.core.Job").add(
"org.springframework.batch", "spring-batch-core",
dependencies.getProperty("spring-batch.version", "2.2.0.RELEASE"));
"spring-batch-core");
dependencies.ifAnyMissingClasses("org.springframework.jdbc.core.JdbcTemplate")
.add("org.springframework", "spring-jdbc",
dependencies.getProperty("spring.version"));
.add("spring-jdbc");
}
@Override

@ -39,8 +39,7 @@ public class SpringBootCompilerAutoConfiguration extends CompilerAutoConfigurati
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies.ifAnyMissingClasses("org.springframework.boot.SpringApplication")
.add("org.springframework.boot", "spring-boot-starter",
dependencies.getProperty("spring-boot.version"));
.add("spring-boot-starter");
}
@Override

@ -45,16 +45,9 @@ public class SpringIntegrationCompilerAutoConfiguration extends CompilerAutoConf
@Override
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies
.ifAnyMissingClasses("org.springframework.integration.Message")
.add("org.springframework.integration", "spring-integration-core",
dependencies.getProperty("spring-integration.version"))
.add("org.springframework.integration",
"spring-integration-dsl-groovy-core",
dependencies.getProperty("spring-integration-dsl.version"));
dependencies.ifAnyMissingClasses("groovy.util.XmlParser").add(
"org.codehaus.groovy", "groovy-xml",
dependencies.getProperty("groovy.version"));
dependencies.ifAnyMissingClasses("org.springframework.integration.Message")
.add("spring-integration-core").add("spring-integration-dsl-groovy-core");
dependencies.ifAnyMissingClasses("groovy.util.XmlParser").add("groovy-xml");
}
@Override

@ -46,11 +46,9 @@ public class SpringMvcCompilerAutoConfiguration extends CompilerAutoConfiguratio
public void applyDependencies(DependencyCustomizer dependencies) {
dependencies
.ifAnyMissingClasses("org.springframework.web.servlet.mvc.Controller")
.add("org.springframework.boot", "spring-boot-starter-web",
dependencies.getProperty("spring-boot.version"));
.add("spring-boot-starter-web");
dependencies.add("org.codehaus.groovy", "groovy-templates",
dependencies.getProperty("groovy.version"));
dependencies.add("groovy-templates");
}
@Override

@ -39,10 +39,7 @@ public class SpringSecurityCompilerAutoConfiguration extends CompilerAutoConfigu
dependencies
.ifAnyMissingClasses(
"org.springframework.security.config.annotation.web.configuration.EnableWebSecurity")
.add("org.springframework.security", "spring-security-config",
dependencies.getProperty("spring-security.version"))
.add("org.springframework.security", "spring-security-web",
dependencies.getProperty("spring-security.version"), false);
.add("spring-security-config").add("spring-security-web", false);
}
@Override

@ -41,10 +41,7 @@ public class TransactionManagementCompilerAutoConfiguration extends
dependencies
.ifAnyMissingClasses(
"org.springframework.transaction.annotation.Transactional")
.add("org.springframework", "spring-tx",
dependencies.getProperty("spring.version"))
.add("org.springframework.boot", "spring-boot-starter-aop",
dependencies.getProperty("spring-boot.version"));
.add("spring-tx").add("spring-boot-starter-aop");
}
@Override

@ -1,11 +0,0 @@
groovy.version: ${groovy.version}
jetty.version: ${jetty.version}
reactor.version: ${reactor.version}
spring.version: ${spring.version}
spring-batch.version: ${spring-batch.version}
spring-boot.version: ${project.version}
spring-rabbit.version: ${spring-rabbit.version}
spring-security.version: ${spring-security.version}
spring-integration.version: ${spring-integration.version}
spring-integration-groovydsl.version: ${spring-integration-groovydsl.version}
tomcat.version: ${tomcat.version}

@ -24,7 +24,12 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.ivy.util.FileUtil;
import org.junit.*;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.OutputCapture;
import org.springframework.boot.cli.command.CleanCommand;
import org.springframework.boot.cli.command.RunCommand;
@ -89,6 +94,13 @@ public class SampleIntegrationTests {
assertTrue("Wrong output: " + output, output.contains("Hello World"));
}
@Test
public void simpleGrabSample() throws Exception {
start("samples/simpleGrab.groovy");
String output = this.outputCapture.getOutputAndRelease();
assertTrue("Wrong output: " + output, output.contains("Hello World"));
}
@Test
public void templateSample() throws Exception {
start("samples/template.groovy");
@ -192,13 +204,14 @@ public class SampleIntegrationTests {
FileUtil.forceDelete(new File("activemq-data")); // cleanup ActiveMQ cruft
}
@Test
@Ignore // this test requires RabbitMQ to be run, so disable it be default
public void rabbitSample() throws Exception {
start("samples/rabbit.groovy");
String output = this.outputCapture.getOutputAndRelease();
assertTrue("Wrong output: " + output,
output.contains("Received Greetings from Spring Boot via RabbitMQ"));
}
@Test
@Ignore
// this test requires RabbitMQ to be run, so disable it be default
public void rabbitSample() throws Exception {
start("samples/rabbit.groovy");
String output = this.outputCapture.getOutputAndRelease();
assertTrue("Wrong output: " + output,
output.contains("Received Greetings from Spring Boot via RabbitMQ"));
}
}

@ -201,6 +201,23 @@
<artifactId>maven-plugin-plugin</artifactId>
<version>3.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>foundrylogic.vpp</groupId>
<artifactId>vpp</artifactId>
<version>2.2.1</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>sonar-maven-plugin</artifactId>
@ -311,6 +328,14 @@
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>objectstyle</id>
<name>ObjectStyle.org Repository</name>
<url>http://objectstyle.org/maven2/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
<profile>

Loading…
Cancel
Save