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