diff --git a/spring-package-maven-plugin/pom.xml b/spring-package-maven-plugin/pom.xml
index 716d7be6ad2..fb552f43f7f 100644
--- a/spring-package-maven-plugin/pom.xml
+++ b/spring-package-maven-plugin/pom.xml
@@ -14,6 +14,11 @@
+
+ ${project.groupId}
+ spring-launcher
+ ${project.version}
+
org.apache.maven
maven-archiver
@@ -82,13 +87,6 @@
maven-plugin-annotations
provided
-
-
- ${project.groupId}
- spring-launcher
- ${project.version}
- test
-
diff --git a/spring-package-maven-plugin/src/it/jar/pom.xml b/spring-package-maven-plugin/src/it/jar/pom.xml
index 0fdabefade8..2d1b98f4763 100644
--- a/spring-package-maven-plugin/src/it/jar/pom.xml
+++ b/spring-package-maven-plugin/src/it/jar/pom.xml
@@ -12,7 +12,6 @@
@project.groupId@
@project.artifactId@
@project.version@
- true
diff --git a/spring-package-maven-plugin/src/it/run/pom.xml b/spring-package-maven-plugin/src/it/run/pom.xml
new file mode 100644
index 00000000000..d2b82255d2d
--- /dev/null
+++ b/spring-package-maven-plugin/src/it/run/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+ org.springframework.zero.maven.it
+ run
+ 0.0.1.BUILD-SNAPSHOT
+ jar
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+
+
+
+ package
+
+ run
+
+
+
+
+
+
+
diff --git a/spring-package-maven-plugin/src/it/run/src/main/java/org/test/SampleApplication.java b/spring-package-maven-plugin/src/it/run/src/main/java/org/test/SampleApplication.java
new file mode 100644
index 00000000000..30c4f3246de
--- /dev/null
+++ b/spring-package-maven-plugin/src/it/run/src/main/java/org/test/SampleApplication.java
@@ -0,0 +1,9 @@
+package org.test;
+
+public class SampleApplication {
+
+ public static void main(String[] args) {
+ System.out.println("I haz been run");
+ }
+
+}
diff --git a/spring-package-maven-plugin/src/it/run/verify.groovy b/spring-package-maven-plugin/src/it/run/verify.groovy
new file mode 100644
index 00000000000..841c4a97de5
--- /dev/null
+++ b/spring-package-maven-plugin/src/it/run/verify.groovy
@@ -0,0 +1,3 @@
+def file = new File(basedir, "build.log")
+return file.text.contains("I haz been run")
+
diff --git a/spring-package-maven-plugin/src/it/war/pom.xml b/spring-package-maven-plugin/src/it/war/pom.xml
index 51060360c05..6d659a6036c 100644
--- a/spring-package-maven-plugin/src/it/war/pom.xml
+++ b/spring-package-maven-plugin/src/it/war/pom.xml
@@ -12,7 +12,6 @@
@project.groupId@
@project.artifactId@
@project.version@
- true
diff --git a/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/AbstractExecutableArchiveMojo.java b/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/AbstractExecutableArchiveMojo.java
new file mode 100644
index 00000000000..0d0174799ef
--- /dev/null
+++ b/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/AbstractExecutableArchiveMojo.java
@@ -0,0 +1,117 @@
+/*
+ * 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.maven.packaging;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.maven.archiver.MavenArchiveConfiguration;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+
+/**
+ * Abstract base class for MOJOs that work with executable archives.
+ *
+ * @author Phillip Webb
+ */
+public abstract class AbstractExecutableArchiveMojo extends AbstractMojo {
+
+ protected static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
+
+ private static final Map ARCHIVE_HELPERS;
+ static {
+ Map helpers = new HashMap();
+ helpers.put("jar", new ExecutableJarHelper());
+ helpers.put("war", new ExecutableWarHelper());
+ ARCHIVE_HELPERS = Collections.unmodifiableMap(helpers);
+ }
+
+ /**
+ * The Maven project.
+ */
+ @Parameter(defaultValue = "${project}", readonly = true, required = true)
+ private MavenProject project;
+
+ /**
+ * 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 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;
+
+ /**
+ * The archive configuration to use. See Maven Archiver
+ * Reference.
+ */
+ @Parameter
+ private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
+
+ protected final ArchiveHelper getArchiveHelper() throws MojoExecutionException {
+ ArchiveHelper helper = ARCHIVE_HELPERS.get(getType());
+ if (helper == null) {
+ throw new MojoExecutionException("Unsupported packaging type: " + getType());
+ }
+ return helper;
+ }
+
+ protected final String getStartClass() throws MojoExecutionException {
+ 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");
+ }
+ return mainClass;
+ }
+
+ protected final MavenProject getProject() {
+ return this.project;
+ }
+
+ protected final String getType() {
+ return this.project.getPackaging();
+ }
+
+ protected final String getExtension() {
+ return getProject().getPackaging();
+ }
+
+ protected final MavenArchiveConfiguration getArchiveConfiguration() {
+ return this.archive;
+ }
+
+ protected final File getClassesDirectory() {
+ return this.classesDirectrory;
+ }
+}
diff --git a/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/ExecutableArchiveMojo.java b/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/ExecutableArchiveMojo.java
index 53941c8b7c7..4605f73e1a8 100644
--- a/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/ExecutableArchiveMojo.java
+++ b/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/ExecutableArchiveMojo.java
@@ -21,17 +21,12 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Enumeration;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-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;
@@ -39,7 +34,6 @@ 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;
-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;
@@ -63,20 +57,10 @@ import org.sonatype.aether.util.artifact.DefaultArtifact;
* @author Phillip Webb
*/
@Mojo(name = "package", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
-public class ExecutableArchiveMojo extends AbstractMojo {
-
- private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
+public class ExecutableArchiveMojo extends AbstractExecutableArchiveMojo {
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
- private static final Map ARCHIVE_HELPERS;
- static {
- Map helpers = new HashMap();
- helpers.put("jar", new ExecutableJarHelper());
- helpers.put("war", new ExecutableWarHelper());
- ARCHIVE_HELPERS = Collections.unmodifiableMap(helpers);
- }
-
/**
* Archiver used to create a JAR file.
*/
@@ -95,12 +79,6 @@ public class ExecutableArchiveMojo extends AbstractMojo {
@Component
private RepositorySystem repositorySystem;
- /**
- * The Maven project.
- */
- @Parameter(defaultValue = "${project}", readonly = true, required = true)
- private MavenProject project;
-
/**
* The Maven session.
*/
@@ -119,13 +97,6 @@ public class ExecutableArchiveMojo extends AbstractMojo {
@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
@@ -134,21 +105,6 @@ public class ExecutableArchiveMojo extends AbstractMojo {
@Parameter
private String classifier;
- /**
- * 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.
*/
@@ -165,25 +121,17 @@ public class ExecutableArchiveMojo extends AbstractMojo {
public void execute() throws MojoExecutionException, MojoFailureException {
File archiveFile = createArchive();
if (this.classifier == null || this.classifier.isEmpty()) {
- this.project.getArtifact().setFile(archiveFile);
+ getProject().getArtifact().setFile(archiveFile);
}
else {
getLog().info(
"Attaching archive: " + archiveFile + ", with classifier: "
+ this.classifier);
- this.projectHelper.attachArtifact(this.project, getType(), this.classifier,
+ this.projectHelper.attachArtifact(getProject(), getType(), this.classifier,
archiveFile);
}
}
- private ArchiveHelper getArchiveHelper() throws MojoExecutionException {
- ArchiveHelper helper = ARCHIVE_HELPERS.get(getType());
- if (helper == null) {
- throw new MojoExecutionException("Unsupported packaging type: " + getType());
- }
- return helper;
- }
-
private File createArchive() throws MojoExecutionException {
File archiveFile = getTargetFile();
MavenArchiver archiver = new MavenArchiver();
@@ -195,11 +143,12 @@ public class ExecutableArchiveMojo extends AbstractMojo {
try {
getLog().info("Modifying archive: " + archiveFile);
- copyContent(archiver, this.project.getArtifact().getFile());
+ copyContent(archiver, getProject().getArtifact().getFile());
addLibs(archiver);
ZipFile zipFile = addLauncherClasses(archiver);
try {
- archiver.createArchive(this.session, this.project, this.archive);
+ archiver.createArchive(this.session, getProject(),
+ getArchiveConfiguration());
return archiveFile;
}
finally {
@@ -211,14 +160,6 @@ public class ExecutableArchiveMojo extends AbstractMojo {
}
}
- private String getType() {
- return this.project.getPackaging();
- }
-
- private String getExtension() {
- return this.project.getPackaging();
- }
-
private void copyContent(MavenArchiver archiver, File file) throws IOException {
FileInputStream input = new FileInputStream(file);
@@ -232,7 +173,6 @@ public class ExecutableArchiveMojo extends AbstractMojo {
Enumeration extends ZipEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
- // TODO: maybe merge manifest instead of skipping it?
if (!entry.isDirectory()
&& !entry.getName().toUpperCase().equals("/META-INF/MANIFEST.MF")) {
ZipResource zipResource = new ZipResource(zipFile, entry);
@@ -252,28 +192,20 @@ public class ExecutableArchiveMojo extends AbstractMojo {
}
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,
+ getArchiveConfiguration().setForced(this.forceCreation);
+ String startClass = getStartClass();
+ getArchiveConfiguration().getManifestEntries().put(MAIN_CLASS_ATTRIBUTE,
getArchiveHelper().getLauncherClass());
- this.archive.getManifestEntries().put(START_CLASS_ATTRIBUTE, mainClass);
+ getArchiveConfiguration().getManifestEntries().put(START_CLASS_ATTRIBUTE,
+ startClass);
}
private void addLibs(MavenArchiver archiver) throws MojoExecutionException {
getLog().info("Adding dependencies");
- for (Artifact artifact : this.project.getArtifacts()) {
+ ArchiveHelper archiveHelper = getArchiveHelper();
+ for (Artifact artifact : getProject().getArtifacts()) {
if (artifact.getFile() != null) {
- String dir = getArchiveHelper().getArtifactDestination(artifact);
+ String dir = archiveHelper.getArtifactDestination(artifact);
if (dir != null) {
getLog().debug("Adding dependency: " + artifact);
archiver.getArchiver().addFile(artifact.getFile(),
@@ -288,8 +220,8 @@ public class ExecutableArchiveMojo extends AbstractMojo {
getLog().info("Adding launcher classes");
try {
List repositories = new ArrayList();
- repositories.addAll(this.project.getRemotePluginRepositories());
- repositories.addAll(this.project.getRemoteProjectRepositories());
+ repositories.addAll(getProject().getRemotePluginRepositories());
+ repositories.addAll(getProject().getRemoteProjectRepositories());
String version = getClass().getPackage().getImplementationVersion();
DefaultArtifact artifact = new DefaultArtifact(
diff --git a/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/RunMojo.java b/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/RunMojo.java
new file mode 100644
index 00000000000..d05f4fd3f0c
--- /dev/null
+++ b/spring-package-maven-plugin/src/main/java/org/springframework/maven/packaging/RunMojo.java
@@ -0,0 +1,217 @@
+/*
+ * 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.maven.packaging;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.model.Resource;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+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;
+
+/**
+ * MOJO that can be used to run a executable archive application directly from Maven.
+ *
+ * @author Phillip Webb
+ */
+@Mojo(name = "run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, requiresDependencyResolution = ResolutionScope.TEST)
+public class RunMojo extends AbstractExecutableArchiveMojo {
+
+ /**
+ * Add maven resources to the classpath directly, this allows live in-place editing or
+ * resources. Since resources will be added directly, and via the target/classes
+ * folder they will appear twice if ClassLoader.getResources() is called. In practice
+ * however most applications call ClassLoader.getResource() which will always return
+ * the first resource.
+ */
+ @Parameter(property = "run.addResources", defaultValue = "true")
+ private boolean addResources;
+
+ /**
+ * Arguments that should be passed to the application.
+ */
+ @Parameter(property = "run.arguments")
+ private String[] arguments;
+
+ /**
+ * Folders that should be added to the classpath.
+ */
+ @Parameter
+ private String[] folders;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ final String startClassName = getStartClass();
+ IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(startClassName);
+ Thread launchThread = new Thread(threadGroup, new LaunchRunner(startClassName,
+ this.arguments), startClassName + ".main()");
+ launchThread.setContextClassLoader(getClassLoader());
+ launchThread.start();
+ join(threadGroup);
+ threadGroup.rethrowUncaughtException();
+ }
+
+ private ClassLoader getClassLoader() throws MojoExecutionException {
+ URL[] urls = getClassPathUrls();
+ return new URLClassLoader(urls);
+ }
+
+ private URL[] getClassPathUrls() throws MojoExecutionException {
+ ArchiveHelper archiveHelper = getArchiveHelper();
+ try {
+ List urls = new ArrayList();
+ addUserDefinedFolders(urls);
+ addResources(urls);
+ addProjectClasses(urls);
+ addDependencies(archiveHelper, urls);
+ return urls.toArray(new URL[urls.size()]);
+ }
+ catch (MalformedURLException ex) {
+ throw new MojoExecutionException("Unable to build classpath", ex);
+ }
+ }
+
+ private void addUserDefinedFolders(List urls) throws MalformedURLException {
+ if (this.folders != null) {
+ for (String folder : this.folders) {
+ urls.add(new File(folder).toURI().toURL());
+ }
+ }
+ }
+
+ private void addResources(List urls) throws MalformedURLException {
+ if (this.addResources) {
+ for (Resource resource : getProject().getResources()) {
+ urls.add(new File(resource.getDirectory()).toURI().toURL());
+ }
+ }
+ }
+
+ private void addProjectClasses(List urls) throws MalformedURLException {
+ urls.add(getClassesDirectory().toURI().toURL());
+ }
+
+ private void addDependencies(ArchiveHelper archiveHelper, List urls)
+ throws MalformedURLException {
+ for (Artifact artifact : getProject().getArtifacts()) {
+ if (artifact.getFile() != null) {
+ if (archiveHelper.getArtifactDestination(artifact) != null) {
+ urls.add(artifact.getFile().toURI().toURL());
+ }
+ }
+ }
+ }
+
+ private void join(ThreadGroup threadGroup) {
+ boolean hasNonDaemonThreads;
+ do {
+ hasNonDaemonThreads = false;
+ Thread[] threads = new Thread[threadGroup.activeCount()];
+ threadGroup.enumerate(threads);
+ for (Thread thread : threads) {
+ if (thread != null && !thread.isDaemon()) {
+ try {
+ hasNonDaemonThreads = true;
+ thread.join();
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+ while (hasNonDaemonThreads);
+ }
+
+ /**
+ * Isolated {@link ThreadGroup} to capture uncaught exceptions.
+ */
+ class IsolatedThreadGroup extends ThreadGroup {
+
+ private Throwable exception;
+
+ public IsolatedThreadGroup(String name) {
+ super(name);
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ if (!(ex instanceof ThreadDeath)) {
+ synchronized (this) {
+ this.exception = (this.exception == null ? ex : this.exception);
+ }
+ getLog().warn(ex);
+ }
+ }
+
+ public synchronized void rethrowUncaughtException() throws MojoExecutionException {
+ if (this.exception != null) {
+ throw new MojoExecutionException("An exception occured while running. "
+ + this.exception.getMessage(), this.exception);
+ }
+ }
+ }
+
+ /**
+ * Runner used to launch the application.
+ */
+ class LaunchRunner implements Runnable {
+
+ private String startClassName;
+ private String[] args;
+
+ public LaunchRunner(String startClassName, String... args) {
+ this.startClassName = startClassName;
+ this.args = (args != null ? args : new String[] {});
+ }
+
+ @Override
+ public void run() {
+ Thread thread = Thread.currentThread();
+ ClassLoader classLoader = thread.getContextClassLoader();
+ try {
+ Class> startClass = classLoader.loadClass(this.startClassName);
+ Method mainMethod = startClass.getMethod("main",
+ new Class[] { String[].class });
+ if (!mainMethod.isAccessible()) {
+ mainMethod.setAccessible(true);
+ }
+ mainMethod.invoke(null, new Object[] { this.args });
+ }
+ catch (NoSuchMethodException ex) {
+ Exception wrappedEx = new Exception(
+ "The specified mainClass doesn't contain a "
+ + "main method with appropriate signature.", ex);
+ thread.getThreadGroup().uncaughtException(thread, wrappedEx);
+ }
+ catch (Exception e) {
+ thread.getThreadGroup().uncaughtException(thread, e);
+ }
+ }
+ }
+
+}
diff --git a/spring-starters/spring-starter-parent/pom.xml b/spring-starters/spring-starter-parent/pom.xml
index 3f9f96f1894..43b9d99162a 100644
--- a/spring-starters/spring-starter-parent/pom.xml
+++ b/spring-starters/spring-starter-parent/pom.xml
@@ -204,17 +204,6 @@
2.3
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 1.2.1
-
- true
- ${start-class}
-
-
-
org.codehaus.mojo
@@ -222,19 +211,27 @@
2.0
-
- org.springframework.zero
- spring-package-maven-plugin
- ${spring.zero.version}
- true
-
-
-
- package
-
-
-
-
+
+
+ org.springframework.zero
+ spring-package-maven-plugin
+ ${spring.zero.version}
+
+
+
+ true
+ true
+
+
+
+
+
+
+ package
+
+
+
+