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 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 + + + +