diff --git a/pom.xml b/pom.xml
index d6f6e7e617..4255ca3221 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,7 @@
spring-bootstrap-actuator
spring-bootstrap-cli
spring-bootstrap-launcher
+ spring-bootstrap-maven-plugin
spring-bootstrap-samples
spring-bootstrap-starters
@@ -192,10 +193,18 @@
maven-install-plugin
2.4
+
+ maven-invoker-plugin
+ 1.8
+
maven-jar-plugin
2.4
+
+ maven-plugin-plugin
+ 3.2
+
maven-resources-plugin
2.6
diff --git a/spring-bootstrap-maven-plugin/pom.xml b/spring-bootstrap-maven-plugin/pom.xml
new file mode 100644
index 0000000000..70f6f70210
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/pom.xml
@@ -0,0 +1,136 @@
+
+
+ 4.0.0
+
+ org.springframework.bootstrap
+ spring-bootstrap-parent
+ 0.5.0.BUILD-SNAPSHOT
+
+ spring-bootstrap-maven-plugin
+ maven-plugin
+
+ 3.0.5
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ true
+ true
+
+
+
+
+
+ maven-plugin-plugin
+
+ configurator
+
+
+
+ default-descriptor
+
+ descriptor
+
+ process-classes
+
+
+ help-descriptor
+
+ helpmojo
+
+ process-classes
+
+
+
+
+ maven-invoker-plugin
+
+ ${project.build.directory}/it
+ src/it/settings.xml
+ ${project.build.directory}/local-repo
+ verify
+ true
+
+
+
+ integration-test
+
+ install
+ run
+
+
+
+
+
+
+
+
+ org.apache.maven
+ maven-plugin-api
+ ${mavenVersion}
+
+
+ org.apache.maven
+ maven-artifact
+ ${mavenVersion}
+
+
+ org.apache.maven
+ maven-model
+ ${mavenVersion}
+
+
+ org.apache.maven
+ maven-core
+ ${mavenVersion}
+
+
+ org.apache.maven
+ maven-settings
+ ${mavenVersion}
+
+
+ org.apache.maven.plugin-tools
+ maven-plugin-annotations
+ 3.2
+ provided
+
+
+ org.apache.maven
+ maven-archiver
+ 2.5
+
+
+ org.ow2.asm
+ asm
+ 4.0
+
+
+ org.codehaus.plexus
+ plexus-archiver
+ 2.2
+
+
+ org.codehaus.plexus
+ plexus-container-default
+
+
+ org.codehaus.plexus
+ plexus-component-api
+
+
+
+
+ ${project.groupId}
+ spring-bootstrap-launcher
+ ${project.version}
+ test
+
+
+
diff --git a/spring-bootstrap-maven-plugin/src/it/executable-jar/pom.xml b/spring-bootstrap-maven-plugin/src/it/executable-jar/pom.xml
new file mode 100644
index 0000000000..8fb31b6972
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/executable-jar/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+ org.springframework.bootstrap.maven.it
+ executable-jar
+ 0.0.1.BUILD-SNAPSHOT
+ executable-jar
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+ true
+
+
+
+
+
+ @project.groupId@
+ spring-bootstrap
+ @project.version@
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.0.1
+ provided
+
+
+
diff --git a/spring-bootstrap-maven-plugin/src/it/executable-jar/src/main/java/org/test/SampleApplication.java b/spring-bootstrap-maven-plugin/src/it/executable-jar/src/main/java/org/test/SampleApplication.java
new file mode 100644
index 0000000000..0b3b431677
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/executable-jar/src/main/java/org/test/SampleApplication.java
@@ -0,0 +1,8 @@
+package org.test;
+
+public class SampleApplication {
+
+ public static void main(String[] args) {
+ }
+
+}
diff --git a/spring-bootstrap-maven-plugin/src/it/executable-jar/verify.groovy b/spring-bootstrap-maven-plugin/src/it/executable-jar/verify.groovy
new file mode 100644
index 0000000000..3cb1dc0b25
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/executable-jar/verify.groovy
@@ -0,0 +1,7 @@
+import java.io.*;
+import org.springframework.bootstrap.maven.*;
+
+Verify.verifyJar(
+ new File( basedir, "target/executable-jar-0.0.1.BUILD-SNAPSHOT.jar" )
+);
+
diff --git a/spring-bootstrap-maven-plugin/src/it/executable-war/pom.xml b/spring-bootstrap-maven-plugin/src/it/executable-war/pom.xml
new file mode 100644
index 0000000000..abcdb0e994
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/executable-war/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+ org.springframework.bootstrap.maven.it
+ executable-war
+ 0.0.1.BUILD-SNAPSHOT
+ executable-war
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+ true
+
+
+
+
+
+ @project.groupId@
+ spring-bootstrap
+ @project.version@
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.0.1
+ provided
+
+
+
diff --git a/spring-bootstrap-maven-plugin/src/it/executable-war/src/main/java/org/test/SampleApplication.java b/spring-bootstrap-maven-plugin/src/it/executable-war/src/main/java/org/test/SampleApplication.java
new file mode 100644
index 0000000000..0b3b431677
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/executable-war/src/main/java/org/test/SampleApplication.java
@@ -0,0 +1,8 @@
+package org.test;
+
+public class SampleApplication {
+
+ public static void main(String[] args) {
+ }
+
+}
diff --git a/spring-bootstrap-maven-plugin/src/it/executable-war/src/main/webapp/index.html b/spring-bootstrap-maven-plugin/src/it/executable-war/src/main/webapp/index.html
new file mode 100644
index 0000000000..18ecdcb795
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/executable-war/src/main/webapp/index.html
@@ -0,0 +1 @@
+
diff --git a/spring-bootstrap-maven-plugin/src/it/executable-war/verify.groovy b/spring-bootstrap-maven-plugin/src/it/executable-war/verify.groovy
new file mode 100644
index 0000000000..22967f9ab3
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/executable-war/verify.groovy
@@ -0,0 +1,7 @@
+import java.io.*;
+import org.springframework.bootstrap.maven.*;
+
+Verify.verifyWar(
+ new File( basedir, "target/executable-war-0.0.1.BUILD-SNAPSHOT.war" )
+);
+
diff --git a/spring-bootstrap-maven-plugin/src/it/settings.xml b/spring-bootstrap-maven-plugin/src/it/settings.xml
new file mode 100644
index 0000000000..e1e0ace341
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/it/settings.xml
@@ -0,0 +1,35 @@
+
+
+
+
+ it-repo
+
+ true
+
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+
diff --git a/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/AbstractExecutableArchiveMojo.java b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/AbstractExecutableArchiveMojo.java
new file mode 100644
index 0000000000..2f086cc150
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/AbstractExecutableArchiveMojo.java
@@ -0,0 +1,318 @@
+/*
+ * 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.bootstrap.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+import org.apache.maven.archiver.MavenArchiveConfiguration;
+import org.apache.maven.archiver.MavenArchiver;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.MavenProjectHelper;
+import org.codehaus.plexus.archiver.Archiver;
+import org.codehaus.plexus.archiver.jar.JarArchiver;
+import org.codehaus.plexus.archiver.zip.ZipEntry;
+import org.codehaus.plexus.archiver.zip.ZipFile;
+import org.codehaus.plexus.archiver.zip.ZipResource;
+import org.sonatype.aether.RepositorySystem;
+import org.sonatype.aether.RepositorySystemSession;
+import org.sonatype.aether.repository.RemoteRepository;
+import org.sonatype.aether.resolution.ArtifactRequest;
+import org.sonatype.aether.resolution.ArtifactResult;
+import org.sonatype.aether.util.artifact.DefaultArtifact;
+
+/**
+ * Abstract base of MOJOs that package executable archives.
+ *
+ * @author Phillip Webb
+ */
+public abstract class AbstractExecutableArchiveMojo extends AbstractMojo {
+
+ private static final String[] DEFAULT_EXCLUDES = new String[] { "**/package.html" };
+
+ private static final String[] DEFAULT_INCLUDES = new String[] { "**/**" };
+
+ private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
+
+ private static final String START_CLASS_ATTRIBUTE = "Start-Class";
+
+ /**
+ * Archiver used to create a JAR file.
+ */
+ @Component(role = Archiver.class, hint = "jar")
+ private JarArchiver jarArchiver;
+
+ /**
+ * Maven project helper utils.
+ */
+ @Component
+ private MavenProjectHelper projectHelper;
+
+ /**
+ * Aether repository system used to download artifacts.
+ */
+ @Component
+ private RepositorySystem repositorySystem;
+
+ /**
+ * The Maven project.
+ */
+ @Parameter(defaultValue = "${project}", readonly = true, required = true)
+ private MavenProject project;
+
+ /**
+ * The Maven session.
+ */
+ @Parameter(defaultValue = "${session}", readonly = true, required = true)
+ private MavenSession session;
+
+ /**
+ * Directory containing the generated archive.
+ */
+ @Parameter(defaultValue = "${project.build.directory}", required = true)
+ private File outputDirectory;
+
+ /**
+ * Name of the generated archive.
+ */
+ @Parameter(defaultValue = "${project.build.finalName}", required = true)
+ private String finalName;
+
+ /**
+ * The name of the main class. If not specified the first compiled class found that
+ * contains a 'main' method will be used.
+ */
+ @Parameter
+ private String mainClass;
+
+ /**
+ * Classifier to add to the artifact generated. If given, the artifact will be
+ * attached. If this is not given,it will merely be written to the output directory
+ * according to the finalName.
+ */
+ @Parameter
+ private String classifier;
+
+ /**
+ * List of files to include. Specified as fileset patterns which are relative to the
+ * input directory whose contents is being packaged into the archive.
+ */
+ @Parameter
+ private String[] includes;
+
+ /**
+ * List of files to exclude. Specified as fileset patterns which are relative to the
+ * input directory whose contents is being packaged into the archive.
+ */
+ @Parameter
+ private String[] excludes;
+
+ /**
+ * Directory containing the classes and resource files that should be packaged into
+ * the archive.
+ */
+ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
+ private File classesDirectrory;
+
+ /**
+ * The archive configuration to use. See Maven Archiver
+ * Reference.
+ */
+ @Parameter
+ private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
+
+ /**
+ * Whether creating the archive should be forced.
+ */
+ @Parameter(property = "archive.forceCreation", defaultValue = "false")
+ private boolean forceCreation;
+
+ /**
+ * The current repository/network configuration of Maven.
+ */
+ @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
+ private RepositorySystemSession repositorySystemSession;
+
+ /**
+ * Returns the type as defined in plexus components.xml
+ */
+ protected abstract String getType();
+
+ /**
+ * Returns the file extension for the archive (e.g. 'jar').
+ */
+ protected abstract String getExtension();
+
+ /**
+ * Returns the destination of an {@link Artifact}.
+ * @param artifact the artifact
+ * @return the destination or {@code null} to exclude
+ */
+ protected abstract String getArtifactDestination(Artifact artifact);
+
+ /**
+ * Returns the launcher class that will be used.
+ */
+ protected abstract String getLauncherClass();
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ File archiveFile = createArchive();
+ if (this.classifier == null || this.classifier.isEmpty()) {
+ this.project.getArtifact().setFile(archiveFile);
+ } else {
+ this.projectHelper.attachArtifact(this.project, getType(), this.classifier,
+ archiveFile);
+ }
+ }
+
+ private File createArchive() throws MojoExecutionException {
+ File archiveFile = getTargetFile();
+ MavenArchiver archiver = new MavenArchiver();
+ customizeArchiveConfiguration();
+
+ archiver.setArchiver(this.jarArchiver);
+ archiver.setOutputFile(archiveFile);
+ archiver.getArchiver().setRecompressAddedZips(false);
+
+ try {
+ addContent(archiver);
+ addLibs(archiver);
+ ZipFile zipFile = addLauncherClasses(archiver);
+ try {
+ archiver.createArchive(this.session, this.project, this.archive);
+ return archiveFile;
+ } finally {
+ zipFile.close();
+ }
+ } catch (Exception ex) {
+ throw new MojoExecutionException("Error assembling archive", ex);
+ }
+ }
+
+ private File getTargetFile() {
+ String classifier = (this.classifier == null ? "" : this.classifier.trim());
+ if (classifier.length() > 0 && !classifier.startsWith("-")) {
+ classifier = "-" + classifier;
+ }
+ return new File(this.outputDirectory, this.finalName + classifier + "."
+ + getExtension());
+ }
+
+ private void customizeArchiveConfiguration() throws MojoExecutionException {
+ this.archive.setForced(this.forceCreation);
+ String mainClass = this.mainClass;
+ if (mainClass == null) {
+ mainClass = this.archive.getManifestEntries().get(MAIN_CLASS_ATTRIBUTE);
+ }
+ if (mainClass == null) {
+ mainClass = MainClassFinder.findMainClass(this.classesDirectrory);
+ }
+ if (mainClass == null) {
+ throw new MojoExecutionException("Unable to find a suitable main class, "
+ + "please add a 'mainClass' property");
+ }
+ this.archive.getManifestEntries().put(MAIN_CLASS_ATTRIBUTE, getLauncherClass());
+ this.archive.getManifestEntries().put(START_CLASS_ATTRIBUTE, mainClass);
+ }
+
+ protected void addContent(MavenArchiver archiver) {
+ if (this.classesDirectrory.exists()) {
+ archiver.getArchiver().addDirectory(this.classesDirectrory,
+ getClassesDirectoryPrefix(), getIncludes(), getExcludes());
+ }
+ }
+
+ protected String getClassesDirectoryPrefix() {
+ return "";
+ }
+
+ protected final String[] getIncludes() {
+ if (this.includes != null && this.includes.length > 0) {
+ return this.includes;
+ }
+ return DEFAULT_INCLUDES;
+ }
+
+ protected final String[] getExcludes() {
+ if (this.excludes != null && this.excludes.length > 0) {
+ return this.excludes;
+ }
+ return DEFAULT_EXCLUDES;
+ }
+
+ private void addLibs(MavenArchiver archiver) {
+ for (Artifact artifact : this.project.getArtifacts()) {
+ if (artifact.getFile() != null) {
+ String dir = getArtifactDestination(artifact);
+ if (dir != null) {
+ archiver.getArchiver().addFile(artifact.getFile(),
+ dir + artifact.getFile().getName());
+ }
+ }
+ }
+ }
+
+ private ZipFile addLauncherClasses(MavenArchiver archiver)
+ throws MojoExecutionException {
+ try {
+ ArtifactRequest request = new ArtifactRequest();
+ String version = getClass().getPackage().getImplementationVersion();
+ request.setArtifact(new DefaultArtifact(
+ "org.springframework.bootstrap:spring-bootstrap-launcher:" + version));
+ List repositories = new ArrayList();
+ repositories.addAll(this.project.getRemotePluginRepositories());
+ repositories.addAll(this.project.getRemoteProjectRepositories());
+ ArtifactResult result = this.repositorySystem.resolveArtifact(
+ this.repositorySystemSession, request);
+ if (result.getArtifact() == null) {
+ throw new MojoExecutionException("Unable to resolve launcher classes");
+ }
+ return addLauncherClasses(archiver, result.getArtifact().getFile());
+ } catch (Exception ex) {
+ if (ex instanceof MojoExecutionException) {
+ throw (MojoExecutionException) ex;
+ }
+ throw new MojoExecutionException("Unable to add launcher classes", ex);
+ }
+ }
+
+ private ZipFile addLauncherClasses(MavenArchiver archiver, File file)
+ throws IOException {
+ ZipFile zipFile = new ZipFile(file);
+ Enumeration extends ZipEntry> entries = zipFile.getEntries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ if (!entry.isDirectory() && !entry.getName().startsWith("/META-INF")) {
+ ZipResource zipResource = new ZipResource(zipFile, entry);
+ archiver.getArchiver().addResource(zipResource, entry.getName(), -1);
+ }
+ }
+ return zipFile;
+ }
+}
diff --git a/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/ExecutableJarMojo.java b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/ExecutableJarMojo.java
new file mode 100644
index 0000000000..6a72111fd9
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/ExecutableJarMojo.java
@@ -0,0 +1,61 @@
+/*
+ * 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.bootstrap.maven;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+
+/**
+ * Build an executable JAR file.
+ *
+ * @author Phillip Webb
+ */
+@Mojo(name = "executable-jar", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
+public class ExecutableJarMojo extends AbstractExecutableArchiveMojo {
+
+ private static final Set LIB_SCOPES = new HashSet(Arrays.asList(
+ "compile", "runtime", "provided"));
+
+ @Override
+ protected String getType() {
+ return "executable-jar";
+ }
+
+ @Override
+ protected String getExtension() {
+ return "jar";
+ }
+
+ @Override
+ protected String getArtifactDestination(Artifact artifact) {
+ if (LIB_SCOPES.contains(artifact.getScope())) {
+ return "lib/";
+ }
+ return null;
+ }
+
+ @Override
+ protected String getLauncherClass() {
+ return "org.springframework.bootstrap.launcher.JarLauncher";
+ }
+}
diff --git a/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/ExecutableWarMojo.java b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/ExecutableWarMojo.java
new file mode 100644
index 0000000000..64bd714409
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/ExecutableWarMojo.java
@@ -0,0 +1,90 @@
+/*
+ * 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.bootstrap.maven;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.maven.archiver.MavenArchiver;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+
+/**
+ * Build an executable WAR file.
+ *
+ * @author Phillip Webb
+ */
+@Mojo(name = "executable-war", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
+public class ExecutableWarMojo extends AbstractExecutableArchiveMojo {
+
+ // FIXME classes in the wrong place
+
+ private static final Map SCOPE_DESTINATIONS;
+ static {
+ Map map = new HashMap();
+ map.put("compile", "WEB-INF/lib/");
+ map.put("runtime", "WEB-INF/lib/");
+ map.put("provided", "WEB-INF/lib-provided/");
+ SCOPE_DESTINATIONS = Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * Single directory for extra files to include in the WAR. This is where you place
+ * your JSP files.
+ */
+ @Parameter(defaultValue = "${basedir}/src/main/webapp", required = true)
+ private File warSourceDirectory;
+
+ @Override
+ protected String getType() {
+ return "executable-war";
+ }
+
+ @Override
+ protected String getExtension() {
+ return "war";
+ }
+
+ @Override
+ protected String getArtifactDestination(Artifact artifact) {
+ return SCOPE_DESTINATIONS.get(artifact.getScope());
+ }
+
+ @Override
+ protected String getClassesDirectoryPrefix() {
+ return "WEB-INF/classes/";
+ }
+
+ @Override
+ protected void addContent(MavenArchiver archiver) {
+ super.addContent(archiver);
+ if (this.warSourceDirectory.exists()) {
+ archiver.getArchiver().addDirectory(this.warSourceDirectory, getIncludes(),
+ getExcludes());
+ }
+ }
+
+ @Override
+ protected String getLauncherClass() {
+ return "org.springframework.bootstrap.launcher.WarLauncher";
+ }
+}
diff --git a/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/MainClassFinder.java b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/MainClassFinder.java
new file mode 100644
index 0000000000..6f9149efb6
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/main/java/org/springframework/bootstrap/maven/MainClassFinder.java
@@ -0,0 +1,156 @@
+/*
+ * 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.bootstrap.maven;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Deque;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Finds any class with a {@code public static main} method by performing a breadth first
+ * directory search.
+ *
+ * @author Phillip Webb
+ */
+class MainClassFinder {
+
+ private static final String DOT_CLASS = ".class";
+
+ private static final Type STRING_ARRAY_TYPE = Type.getType(String[].class);
+
+ private static final Type MAIN_METHOD_TYPE = Type.getMethodType(Type.VOID_TYPE,
+ STRING_ARRAY_TYPE);
+
+ private static final String MAIN_METHOD_NAME = "main";
+
+ private static final FileFilter CLASS_FILE_FILTER = new FileFilter() {
+ @Override
+ public boolean accept(File file) {
+ return (file.isFile() && file.getName().endsWith(DOT_CLASS));
+ }
+ };
+
+ private static final FileFilter PACKAGE_FOLDER_FILTER = new FileFilter() {
+ @Override
+ public boolean accept(File file) {
+ return file.isDirectory() && !file.getName().startsWith(".");
+ }
+ };
+
+ public static String findMainClass(File root) {
+ File mainClassFile = findMainClassFile(root);
+ if (mainClassFile == null) {
+ return null;
+ }
+ String mainClass = mainClassFile.getAbsolutePath().substring(
+ root.getAbsolutePath().length() + 1);
+ mainClass = mainClass.replace('/', '.');
+ mainClass = mainClass.replace('\\', '.');
+ mainClass = mainClass.substring(0, mainClass.length() - DOT_CLASS.length());
+ return mainClass;
+ }
+
+ public static File findMainClassFile(File root) {
+ Deque stack = new ArrayDeque();
+ stack.push(root);
+ while (!stack.isEmpty()) {
+ File file = stack.pop();
+ if (isMainClassFile(file)) {
+ return file;
+ }
+ if (file.isDirectory()) {
+ pushAllSorted(stack, file.listFiles(PACKAGE_FOLDER_FILTER));
+ pushAllSorted(stack, file.listFiles(CLASS_FILE_FILTER));
+ }
+ }
+ return null;
+ }
+
+ private static boolean isMainClassFile(File file) {
+ try {
+ InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
+ try {
+ ClassReader classReader = new ClassReader(inputStream);
+ MainMethodFinder mainMethodFinder = new MainMethodFinder();
+ classReader.accept(mainMethodFinder, ClassReader.SKIP_CODE);
+ return mainMethodFinder.isFound();
+ } finally {
+ inputStream.close();
+ }
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+
+ private static void pushAllSorted(Deque stack, File[] files) {
+ Arrays.sort(files, new Comparator() {
+ @Override
+ public int compare(File o1, File o2) {
+ return o1.getName().compareTo(o2.getName());
+ }
+ });
+ for (File file : files) {
+ stack.push(file);
+ }
+ }
+
+ private static class MainMethodFinder extends ClassVisitor {
+
+ private boolean found;
+
+ public MainMethodFinder() {
+ super(Opcodes.ASM4);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc,
+ String signature, String[] exceptions) {
+ if (isAccess(access, Opcodes.ACC_PUBLIC, Opcodes.ACC_STATIC)
+ && MAIN_METHOD_NAME.equals(name)
+ && MAIN_METHOD_TYPE.getDescriptor().equals(desc)) {
+ this.found = true;
+ }
+ return null;
+ }
+
+ private boolean isAccess(int access, int... requiredOpsCodes) {
+ for (int requiredOpsCode : requiredOpsCodes) {
+ if ((access & requiredOpsCode) == 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean isFound() {
+ return this.found;
+ }
+ }
+}
diff --git a/spring-bootstrap-maven-plugin/src/main/resources/META-INF/plexus/components.xml b/spring-bootstrap-maven-plugin/src/main/resources/META-INF/plexus/components.xml
new file mode 100644
index 0000000000..8ed12cc6f9
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/main/resources/META-INF/plexus/components.xml
@@ -0,0 +1,74 @@
+
+
+
+ org.apache.maven.artifact.handler.ArtifactHandler
+ executable-war
+ org.apache.maven.artifact.handler.DefaultArtifactHandler
+
+
+ executable-jar
+ jar
+ java
+ true
+
+
+
+ org.apache.maven.lifecycle.mapping.LifecycleMapping
+ executable-jar
+ org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping
+
+
+
+
+ default
+
+ org.apache.maven.plugins:maven-resources-plugin:resources
+ org.apache.maven.plugins:maven-compiler-plugin:compile
+ org.apache.maven.plugins:maven-resources-plugin:testResources
+ org.apache.maven.plugins:maven-compiler-plugin:testCompile
+ org.apache.maven.plugins:maven-surefire-plugin:test
+ org.springframework.bootstrap:spring-bootstrap-maven-plugin:executable-jar
+ org.apache.maven.plugins:maven-install-plugin:install
+ org.apache.maven.plugins:maven-deploy-plugin:deploy
+
+
+
+
+
+
+ org.apache.maven.artifact.handler.ArtifactHandler
+ executable-war
+ org.apache.maven.artifact.handler.DefaultArtifactHandler
+
+
+ executable-war
+ war
+ java
+ true
+
+
+
+ org.apache.maven.lifecycle.mapping.LifecycleMapping
+ executable-war
+ org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping
+
+
+
+
+ default
+
+ org.apache.maven.plugins:maven-resources-plugin:resources
+ org.apache.maven.plugins:maven-compiler-plugin:compile
+ org.apache.maven.plugins:maven-resources-plugin:testResources
+ org.apache.maven.plugins:maven-compiler-plugin:testCompile
+ org.apache.maven.plugins:maven-surefire-plugin:test
+ org.springframework.bootstrap:spring-bootstrap-maven-plugin:executable-war
+ org.apache.maven.plugins:maven-install-plugin:install
+ org.apache.maven.plugins:maven-deploy-plugin:deploy
+
+
+
+
+
+
+
diff --git a/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/MainClassFinderTests.java b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/MainClassFinderTests.java
new file mode 100644
index 0000000000..d83e29af72
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/MainClassFinderTests.java
@@ -0,0 +1,94 @@
+/*
+ * 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.bootstrap.maven;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.codehaus.plexus.util.IOUtil;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.springframework.bootstrap.maven.sample.ClassWithMainMethod;
+import org.springframework.bootstrap.maven.sample.ClassWithoutMainMethod;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Tests for {@link MainClassFinder}.
+ *
+ * @author Phillip Webb
+ */
+public class MainClassFinderTests {
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void findMainClass() throws Exception {
+ File expected = copyToTemp("b.class", ClassWithMainMethod.class);
+ copyToTemp("a.class", ClassWithoutMainMethod.class);
+ File actual = MainClassFinder.findMainClassFile(this.temporaryFolder.getRoot());
+ assertThat(actual, equalTo(expected));
+ }
+
+ @Test
+ public void findMainClassInSubFolder() throws Exception {
+ File expected = copyToTemp("a/b/c/d.class", ClassWithMainMethod.class);
+ copyToTemp("a/b/c/e.class", ClassWithoutMainMethod.class);
+ copyToTemp("a/b/f.class", ClassWithoutMainMethod.class);
+ File actual = MainClassFinder.findMainClassFile(this.temporaryFolder.getRoot());
+ assertThat(actual, equalTo(expected));
+ }
+
+ @Test
+ public void usesBreadthFirst() throws Exception {
+ File expected = copyToTemp("a/b.class", ClassWithMainMethod.class);
+ copyToTemp("a/b/c/e.class", ClassWithMainMethod.class);
+ File actual = MainClassFinder.findMainClassFile(this.temporaryFolder.getRoot());
+ assertThat(actual, equalTo(expected));
+ }
+
+ @Test
+ public void findsClassName() throws Exception {
+ copyToTemp("org/test/MyApp.class", ClassWithMainMethod.class);
+ assertThat(MainClassFinder.findMainClass(this.temporaryFolder.getRoot()),
+ equalTo("org.test.MyApp"));
+
+ }
+
+ private File copyToTemp(String filename, Class> classToCopy) throws IOException {
+ String[] paths = filename.split("\\/");
+ File file = this.temporaryFolder.getRoot();
+ for (String path : paths) {
+ file = new File(file, path);
+ }
+ file.getParentFile().mkdirs();
+ InputStream inputStream = getClass().getResourceAsStream(
+ "/" + classToCopy.getName().replace(".", "/") + ".class");
+ OutputStream outputStream = new FileOutputStream(file);
+ try {
+ IOUtil.copy(inputStream, outputStream);
+ } finally {
+ outputStream.close();
+ }
+ return file;
+ }
+}
diff --git a/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/Verify.java b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/Verify.java
new file mode 100644
index 0000000000..8d77cddb9d
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/Verify.java
@@ -0,0 +1,161 @@
+/*
+ * 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.bootstrap.maven;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.springframework.bootstrap.launcher.JarLauncher;
+import org.springframework.bootstrap.launcher.WarLauncher;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Verification utility for use with maven-invoker-plugin verification scripts.
+ *
+ * @author Phillip Webb
+ */
+public class Verify {
+
+ public static void main(String[] args) {
+ try {
+ verifyJar(new File(
+ "/Users/pwebb/projects/spring/spring-bootstrap/code/spring-bootstrap-maven-plugin/target/it/executable-jar/target/executable-jar-0.0.1.BUILD-SNAPSHOT.jar"));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ public static void verifyJar(File file) throws Exception {
+ new JarArchiveVerification(file).verify();
+ }
+
+ public static void verifyWar(File file) throws Exception {
+ new WarArchiveVerification(file).verify();
+ }
+
+ private static abstract class AbstractArchiveVerification {
+
+ private File file;
+
+ public AbstractArchiveVerification(File file) {
+ this.file = file;
+ }
+
+ public void verify() throws Exception {
+ assertTrue("Archive missing", this.file.exists());
+ assertTrue("Archive not a file", this.file.isFile());
+
+ ZipFile zipFile = new ZipFile(this.file);
+ Enumeration extends ZipEntry> entries = zipFile.entries();
+ Map zipMap = new HashMap();
+ while (entries.hasMoreElements()) {
+ ZipEntry zipEntry = entries.nextElement();
+ zipMap.put(zipEntry.getName(), zipEntry);
+ }
+ verifyZipEntries(zipFile, zipMap);
+ zipFile.close();
+ }
+
+ protected void verifyZipEntries(ZipFile zipFile, Map entries)
+ throws Exception {
+ verifyManifest(zipFile, entries.get("META-INF/MANIFEST.MF"));
+ }
+
+ private void verifyManifest(ZipFile zipFile, ZipEntry zipEntry) throws Exception {
+ Manifest manifest = new Manifest(zipFile.getInputStream(zipEntry));
+ verifyManifest(manifest);
+ }
+
+ protected abstract void verifyManifest(Manifest manifest) throws Exception;
+
+ protected final void assertHasEntryNameStartingWith(
+ Map entries, String value) {
+ for (String name : entries.keySet()) {
+ if (name.startsWith(value)) {
+ return;
+ }
+ }
+ throw new IllegalStateException("Expected entry starting with " + value);
+ }
+ }
+
+ private static class JarArchiveVerification extends AbstractArchiveVerification {
+
+ public JarArchiveVerification(File file) {
+ super(file);
+ }
+
+ @Override
+ protected void verifyZipEntries(ZipFile zipFile, Map entries)
+ throws Exception {
+ super.verifyZipEntries(zipFile, entries);
+ assertHasEntryNameStartingWith(entries, "lib/spring-bootstrap");
+ assertHasEntryNameStartingWith(entries, "lib/spring-core");
+ assertHasEntryNameStartingWith(entries, "lib/javax.servlet-api-3.0.1.jar");
+ assertTrue("Unpacked launcher classes", entries.containsKey("org/"
+ + "springframework/bootstrap/launcher/JarLauncher.class"));
+ assertTrue("Own classes", entries.containsKey("org/"
+ + "test/SampleApplication.class"));
+ }
+
+ @Override
+ protected void verifyManifest(Manifest manifest) throws Exception {
+ assertEquals(JarLauncher.class.getName(), manifest.getMainAttributes()
+ .getValue("Main-Class"));
+ assertEquals("org.test.SampleApplication", manifest.getMainAttributes()
+ .getValue("Start-Class"));
+ }
+ }
+
+ private static class WarArchiveVerification extends AbstractArchiveVerification {
+
+ public WarArchiveVerification(File file) {
+ super(file);
+ }
+
+ @Override
+ protected void verifyZipEntries(ZipFile zipFile, Map entries)
+ throws Exception {
+ super.verifyZipEntries(zipFile, entries);
+ assertHasEntryNameStartingWith(entries, "WEB-INF/lib/spring-bootstrap");
+ assertHasEntryNameStartingWith(entries, "WEB-INF/lib/spring-core");
+ assertHasEntryNameStartingWith(entries,
+ "WEB-INF/lib-provided/javax.servlet-api-3.0.1.jar");
+ assertTrue("Unpacked launcher classes", entries.containsKey("org/"
+ + "springframework/bootstrap/launcher/JarLauncher.class"));
+ assertTrue("Own classes", entries.containsKey("WEB-INF/classes/org/"
+ + "test/SampleApplication.class"));
+ assertTrue("Web content", entries.containsKey("index.html"));
+ }
+
+ @Override
+ protected void verifyManifest(Manifest manifest) throws Exception {
+ assertEquals(WarLauncher.class.getName(), manifest.getMainAttributes()
+ .getValue("Main-Class"));
+ assertEquals("org.test.SampleApplication", manifest.getMainAttributes()
+ .getValue("Start-Class"));
+ }
+ }
+
+}
diff --git a/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/sample/ClassWithMainMethod.java b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/sample/ClassWithMainMethod.java
new file mode 100644
index 0000000000..99b3621621
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/sample/ClassWithMainMethod.java
@@ -0,0 +1,29 @@
+/*
+ * 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.bootstrap.maven.sample;
+
+/**
+ * Sample class with a main method.
+ *
+ * @author Phillip Webb
+ */
+public class ClassWithMainMethod {
+
+ public static void main(String[] args) {
+ }
+
+}
diff --git a/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/sample/ClassWithoutMainMethod.java b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/sample/ClassWithoutMainMethod.java
new file mode 100644
index 0000000000..8d9166de32
--- /dev/null
+++ b/spring-bootstrap-maven-plugin/src/test/java/org/springframework/bootstrap/maven/sample/ClassWithoutMainMethod.java
@@ -0,0 +1,25 @@
+/*
+ * 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.bootstrap.maven.sample;
+
+/**
+ * Sample class without a main method.
+ *
+ * @author Phillip Webb
+ */
+public class ClassWithoutMainMethod {
+
+}