diff --git a/pom.xml b/pom.xml
index 9590322308e..a5d47378892 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,7 @@
spring-boot
spring-boot-autoconfigure
spring-boot-loader
+ spring-boot-loader-tools
spring-boot-maven-plugin
spring-boot-ops
spring-boot-ups
@@ -129,6 +130,11 @@
maven-war-plugin
2.3
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 1.8
+
org.codehaus.mojo
exec-maven-plugin
@@ -640,6 +646,11 @@
snakeyaml
1.12
+
+ org.zeroturnaround
+ zt-zip
+ 1.6
+
diff --git a/spring-boot-loader-tools/pom.xml b/spring-boot-loader-tools/pom.xml
new file mode 100644
index 00000000000..f54981aacab
--- /dev/null
+++ b/spring-boot-loader-tools/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-parent
+ 0.5.0.BUILD-SNAPSHOT
+
+ spring-boot-loader-tools
+ jar
+
+ ${basedir}/..
+
+
+
+
+ org.ow2.asm
+ asm
+
+
+
+ org.zeroturnaround
+ zt-zip
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ include-layout-jar
+ generate-resources
+
+ copy
+
+
+
+
+ ${project.groupId}
+ spring-boot-loader
+ ${project.version}
+ spring-boot-loader.jar
+
+
+ ${basedir}/target/generated-resources/loader/META-INF/loader
+ false
+ true
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-resources
+ generate-resources
+
+ add-resource
+
+
+
+
+ ${basedir}/target/generated-resources/loader
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/JarWriter.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/JarWriter.java
new file mode 100644
index 00000000000..65c37ff7aa8
--- /dev/null
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/JarWriter.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+
+/**
+ * Writes JAR content, ensuring valid directory entries are always create and duplicate
+ * items are ignored.
+ *
+ * @author Phillip Webb
+ */
+class JarWriter {
+
+ private static final String NESTED_LOADER_JAR = "/META-INF/loader/spring-boot-loader.jar";
+
+ private static final int BUFFER_SIZE = 4096;
+
+ private final JarOutputStream jarOutput;
+
+ private final Set writtenEntries = new HashSet();
+
+ /**
+ * Create a new {@link JarWriter} instance.
+ * @param file the file to write
+ * @throws IOException
+ * @throws FileNotFoundException
+ */
+ public JarWriter(File file) throws FileNotFoundException, IOException {
+ this.jarOutput = new JarOutputStream(new FileOutputStream(file));
+ }
+
+ /**
+ * Write the specified manifest.
+ * @param manifest the manifest to write
+ * @throws IOException
+ */
+ public void writeManifest(final Manifest manifest) throws IOException {
+ JarEntry entry = new JarEntry("META-INF/MANIFEST.MF");
+ writeEntry(entry, new EntryWriter() {
+ @Override
+ public void write(OutputStream outputStream) throws IOException {
+ manifest.write(outputStream);
+ }
+ });
+ }
+
+ /**
+ * Write all entries from the specified jar file.
+ * @param jarFile the source jar file
+ * @throws IOException
+ */
+ public void writeEntries(JarFile jarFile) throws IOException {
+ Enumeration entries = jarFile.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ EntryWriter entryWriter = new InputStreamEntryWriter(
+ jarFile.getInputStream(entry), true);
+ writeEntry(entry, entryWriter);
+ }
+ }
+
+ /**
+ * Write a nested library.
+ * @param destination the destination of the library
+ * @param file the library file
+ * @throws IOException
+ */
+ public void writeNestedLibrary(String destination, File file) throws IOException {
+ JarEntry entry = new JarEntry(destination + file.getName());
+ entry.setSize(file.length());
+ entry.setCompressedSize(file.length());
+ entry.setCrc(getCrc(file));
+ entry.setMethod(ZipEntry.STORED);
+ writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true));
+ }
+
+ private long getCrc(File file) throws IOException {
+ FileInputStream inputStream = new FileInputStream(file);
+ try {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ CRC32 crc = new CRC32();
+ int bytesRead = -1;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ crc.update(buffer, 0, bytesRead);
+ }
+ return crc.getValue();
+ }
+ finally {
+ inputStream.close();
+ }
+ }
+
+ /**
+ * Write the required spring-boot-loader classes to the JAR.
+ * @throws IOException
+ */
+ public void writeLoaderClasses() throws IOException {
+ JarInputStream inputStream = new JarInputStream(getClass().getResourceAsStream(
+ NESTED_LOADER_JAR));
+ JarEntry entry;
+ while ((entry = inputStream.getNextJarEntry()) != null) {
+ if (entry.getName().endsWith(".class")) {
+ writeEntry(entry, new InputStreamEntryWriter(inputStream, false));
+ }
+ }
+ inputStream.close();
+ }
+
+ /**
+ * Close the writer.
+ * @throws IOException
+ */
+ public void close() throws IOException {
+ this.jarOutput.close();
+ }
+
+ /**
+ * Perform the actual write of a {@link JarEntry}. All other {@code write} method
+ * delegate to this one.
+ * @param entry the entry to write
+ * @param entryWriter the entry writer or {@code null} if there is no content
+ * @throws IOException
+ */
+ private void writeEntry(JarEntry entry, EntryWriter entryWriter) throws IOException {
+ String parent = entry.getName();
+ if (parent.endsWith("/")) {
+ parent = parent.substring(0, parent.length() - 1);
+ }
+ if (parent.lastIndexOf("/") != -1) {
+ parent = parent.substring(0, parent.lastIndexOf("/") + 1);
+ if (parent.length() > 0) {
+ writeEntry(new JarEntry(parent), null);
+ }
+ }
+
+ if (this.writtenEntries.add(entry.getName())) {
+ this.jarOutput.putNextEntry(entry);
+ if (entryWriter != null) {
+ entryWriter.write(this.jarOutput);
+ }
+ this.jarOutput.closeEntry();
+ }
+ }
+
+ /**
+ * Interface used to write jar entry date.
+ */
+ private static interface EntryWriter {
+
+ /**
+ * Write entry data to the specified output stream
+ * @param outputStream the destination for the data
+ * @throws IOException
+ */
+ void write(OutputStream outputStream) throws IOException;
+
+ }
+
+ /**
+ * {@link EntryWriter} that writes content from an {@link InputStream}.
+ */
+ private static class InputStreamEntryWriter implements EntryWriter {
+
+ private final InputStream inputStream;
+
+ private final boolean close;
+
+ public InputStreamEntryWriter(InputStream inputStream, boolean close) {
+ this.inputStream = inputStream;
+ this.close = close;
+ }
+
+ public void write(OutputStream outputStream) throws IOException {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int bytesRead = -1;
+ while ((bytesRead = this.inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ outputStream.flush();
+ if (this.close) {
+ this.inputStream.close();
+ }
+ }
+
+ }
+
+}
diff --git a/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layout.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layout.java
new file mode 100644
index 00000000000..d52db093432
--- /dev/null
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layout.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+/**
+ * Strategy interface used to determine the layout for a particular type of archive.
+ *
+ * @author Phillip Webb
+ * @see Layouts
+ */
+public interface Layout {
+
+ /**
+ * Returns the launcher class name for this layout.
+ * @return the launcher class name
+ */
+ String getLauncherClassName();
+
+ /**
+ * Returns the destination path for a given library.
+ * @param libraryName the name of the library (excluding any path)
+ * @param scope the scope of the library
+ * @return the destination relative to the root of the archive (should end with '/')
+ * or {@code null} if the library should not be included.
+ */
+ String getLibraryDestination(String libraryName, LibraryScope scope);
+
+ /**
+ * Returns the location of classes within the archive.
+ */
+ String getClassesLocation();
+
+}
diff --git a/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layouts.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layouts.java
new file mode 100644
index 00000000000..f6c1693da68
--- /dev/null
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layouts.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Common {@link Layout}s.
+ *
+ * @author Phillip Webb
+ */
+public class Layouts {
+
+ /**
+ * Return the a layout for the given source file.
+ * @param file the source file
+ * @return a {@link Layout}
+ */
+ public static Layout forFile(File file) {
+ if (file == null) {
+ throw new IllegalArgumentException("File must not be null");
+ }
+ if (file.getName().toLowerCase().endsWith(".jar")) {
+ return new Jar();
+ }
+ if (file.getName().toLowerCase().endsWith(".war")) {
+ return new War();
+ }
+ throw new IllegalStateException("Unable to deduce layout for '" + file + "'");
+ }
+
+ /**
+ * Executable JAR layout.
+ */
+ public static class Jar implements Layout {
+
+ @Override
+ public String getLauncherClassName() {
+ return "org.springframework.boot.loader.JarLauncher";
+ }
+
+ @Override
+ public String getLibraryDestination(String libraryName, LibraryScope scope) {
+ return "lib/";
+ }
+
+ @Override
+ public String getClassesLocation() {
+ return "";
+ }
+ }
+
+ /**
+ * Executable WAR layout.
+ */
+ public static class War implements Layout {
+
+ private static final Map SCOPE_DESTINATIONS;
+ static {
+ Map map = new HashMap();
+ map.put(LibraryScope.COMPILE, "WEB-INF/lib/");
+ map.put(LibraryScope.RUNTIME, "WEB-INF/lib/");
+ map.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/");
+ SCOPE_DESTINATIONS = Collections.unmodifiableMap(map);
+ }
+
+ @Override
+ public String getLauncherClassName() {
+ return "org.springframework.boot.loader.WarLauncher";
+ }
+
+ @Override
+ public String getLibraryDestination(String libraryName, LibraryScope scope) {
+ return SCOPE_DESTINATIONS.get(scope);
+ }
+
+ @Override
+ public String getClassesLocation() {
+ return "WEB-INF/classes/";
+ }
+ }
+
+}
diff --git a/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Libraries.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Libraries.java
new file mode 100644
index 00000000000..c1c2ab1372f
--- /dev/null
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Libraries.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.IOException;
+
+/**
+ * Encapsulates information about libraries that may be packed into the archive.
+ *
+ * @author Phillip Webb
+ */
+public interface Libraries {
+
+ /**
+ * Iterate all relevant libraries.
+ * @param callback a callback for each relevant library.
+ * @throws IOException
+ */
+ void doWithLibraries(LibraryCallback callback) throws IOException;
+
+}
diff --git a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArchiveHelper.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryCallback.java
similarity index 56%
rename from spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArchiveHelper.java
rename to spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryCallback.java
index 9e961832c33..3e621fd0024 100644
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArchiveHelper.java
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryCallback.java
@@ -14,27 +14,24 @@
* limitations under the License.
*/
-package org.springframework.boot.maven;
+package org.springframework.boot.launcher.tools;
-import org.apache.maven.artifact.Artifact;
+import java.io.File;
+import java.io.IOException;
/**
- * Strategy interface used by {@link ExecutableArchiveMojo} when creating archives.
+ * Callback interface used to iterate {@link Libraries}.
*
- * @author Dave Syer
+ * @author Phillip Webb
*/
-public interface ArchiveHelper {
+public interface LibraryCallback {
/**
- * Returns the destination of an {@link Artifact}.
- * @param artifact the artifact
- * @return the destination or {@code null} to exclude
+ * Callback to for a single library backed by a {@link File}.
+ * @param file the library file
+ * @param scope the scope of the library
+ * @throws IOException
*/
- String getArtifactDestination(Artifact artifact);
-
- /**
- * Returns the launcher class that will be used.
- */
- String getLauncherClass();
+ void library(File file, LibraryScope scope) throws IOException;
}
diff --git a/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryScope.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryScope.java
new file mode 100644
index 00000000000..9021a679d93
--- /dev/null
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryScope.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+/**
+ * The scope of a library. The common {@link #COMPILE}, {@link #RUNTIME} and
+ * {@link #PROVIDED} scopes are defined here and supported by the common {@link Layouts}.
+ * A custom {@link Layout} can handle additional scopes as required.
+ *
+ * @author Phillip Webb
+ */
+public interface LibraryScope {
+
+ /**
+ * The library is used at compile time and runtime.
+ */
+ public static final LibraryScope COMPILE = new LibraryScope() {
+ @Override
+ public String toString() {
+ return "compile";
+ };
+ };
+
+ /**
+ * The library is used at runtime but not needed for compile.
+ */
+ public static final LibraryScope RUNTIME = new LibraryScope() {
+ @Override
+ public String toString() {
+ return "runtime";
+ };
+ };
+
+ /**
+ * The library is needed for compile but is usually provided when running.
+ */
+ public static final LibraryScope PROVIDED = new LibraryScope() {
+ @Override
+ public String toString() {
+ return "provided";
+ };
+ };
+
+}
diff --git a/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/MainClassFinder.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/MainClassFinder.java
new file mode 100644
index 00000000000..c555aa1d102
--- /dev/null
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/MainClassFinder.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+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.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+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
+ * search.
+ *
+ * @author Phillip Webb
+ */
+public abstract 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(".");
+ }
+ };
+
+ /**
+ * Find the main class from a given folder.
+ * @param rootFolder the root folder to search
+ * @return the main class or {@code null}
+ * @throws IOException
+ */
+ public static String findMainClass(File rootFolder) throws IOException {
+ if (!rootFolder.isDirectory()) {
+ throw new IllegalArgumentException("Inavlid root folder '" + rootFolder + "'");
+ }
+ File mainClassFile = findMainClassFile(rootFolder);
+ if (mainClassFile == null) {
+ return null;
+ }
+ String mainClass = mainClassFile.getAbsolutePath();
+ return convertToClassName(mainClass, rootFolder.getAbsolutePath() + "/");
+ }
+
+ private static File findMainClassFile(File root) throws IOException {
+ Deque stack = new ArrayDeque();
+ stack.push(root);
+ while (!stack.isEmpty()) {
+ File file = stack.pop();
+ if (file.isFile()) {
+ InputStream inputStream = new FileInputStream(file);
+ try {
+ if (isMainClass(inputStream)) {
+ return file;
+ }
+ }
+ finally {
+ inputStream.close();
+ }
+ }
+ if (file.isDirectory()) {
+ pushAllSorted(stack, file.listFiles(PACKAGE_FOLDER_FILTER));
+ pushAllSorted(stack, file.listFiles(CLASS_FILE_FILTER));
+ }
+ }
+ return null;
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * Find the main class in a given jar file.
+ * @param jarFile the jar file to search
+ * @param classesLocation the location within the jar containing classes
+ * @return the main class or {@code null}
+ * @throws IOException
+ */
+ public static String findMainClass(JarFile jarFile, String classesLocation)
+ throws IOException {
+ List classEntries = getClassEntries(jarFile, classesLocation);
+ Collections.sort(classEntries, new ClassEntryComparator());
+ for (JarEntry entry : classEntries) {
+ InputStream inputStream = new BufferedInputStream(
+ jarFile.getInputStream(entry));
+ try {
+ if (isMainClass(inputStream)) {
+ String name = entry.getName();
+ name = convertToClassName(name, classesLocation);
+ return name;
+ }
+ }
+ finally {
+ inputStream.close();
+ }
+ }
+ return null;
+ }
+
+ private static String convertToClassName(String name, String prefix) {
+ name = name.replace("/", ".");
+ name = name.replace('\\', '.');
+ name = name.substring(0, name.length() - DOT_CLASS.length());
+ if (prefix != null) {
+ name = name.substring(prefix.length());
+ }
+ return name;
+ }
+
+ private static List getClassEntries(JarFile source, String classesLocation) {
+ classesLocation = (classesLocation != null ? classesLocation : "");
+ Enumeration sourceEntries = source.entries();
+ List classEntries = new ArrayList();
+ while (sourceEntries.hasMoreElements()) {
+ JarEntry entry = sourceEntries.nextElement();
+ if (entry.getName().startsWith(classesLocation)
+ && entry.getName().endsWith(DOT_CLASS)) {
+ classEntries.add(entry);
+ }
+ }
+ return classEntries;
+ }
+
+ private static boolean isMainClass(InputStream inputStream) {
+ try {
+ ClassReader classReader = new ClassReader(inputStream);
+ MainMethodFinder mainMethodFinder = new MainMethodFinder();
+ classReader.accept(mainMethodFinder, ClassReader.SKIP_CODE);
+ return mainMethodFinder.isFound();
+ }
+ catch (IOException ex) {
+ return false;
+ }
+ }
+
+ private static class ClassEntryComparator implements Comparator {
+
+ @Override
+ public int compare(JarEntry o1, JarEntry o2) {
+ Integer d1 = getDepth(o1);
+ Integer d2 = getDepth(o2);
+ int depthCompare = d1.compareTo(d2);
+ if (depthCompare != 0) {
+ return depthCompare;
+ }
+ return o1.getName().compareTo(o2.getName());
+ }
+
+ private int getDepth(JarEntry entry) {
+ return entry.getName().split("/").length;
+ }
+
+ }
+
+ 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-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Repackager.java b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Repackager.java
new file mode 100644
index 00000000000..33201b09c81
--- /dev/null
+++ b/spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Repackager.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+/**
+ * Utility class that can be used to repackage an archive so that it can be executed using
+ * '{@literal java -jar}'.
+ *
+ * @author Phillip Webb
+ */
+public class Repackager {
+
+ private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
+
+ private static final String START_CLASS_ATTRIBUTE = "Start-Class";
+
+ private String mainClass;
+
+ private boolean backupSource = true;
+
+ private final File source;
+
+ private Layout layout;
+
+ public Repackager(File source) {
+ if (source == null || !source.exists() || !source.isFile()) {
+ throw new IllegalArgumentException("Source must refer to an existing file");
+ }
+ this.source = source.getAbsoluteFile();
+ this.layout = Layouts.forFile(source);
+ }
+
+ /**
+ * Sets the main class that should be run. If not specified the value from the
+ * MANIFEST will be used, or if no manifest entry is found a class the archive will be
+ * searched for a suitable class.
+ * @param mainClass the main class name
+ */
+ public void setMainClass(String mainClass) {
+ this.mainClass = mainClass;
+ }
+
+ /**
+ * Sets if source files should be backed up when they would be overwritten.
+ * @param backupSource if source files should be backed up
+ */
+ public void setBackupSource(boolean backupSource) {
+ this.backupSource = backupSource;
+ }
+
+ /**
+ * Sets the layout to use for the jar. Defaults to {@link Layouts#forFile(File)}.
+ * @param layout the layout
+ */
+ public void setLayout(Layout layout) {
+ if (layout == null) {
+ throw new IllegalArgumentException("Layout must not be null");
+ }
+ this.layout = layout;
+ }
+
+ /**
+ * Repackage the source file so that it can be run using '{@literal java -jar}'
+ * @param libraries the libraries required to run the archive
+ * @throws IOException
+ */
+ public void repackage(Libraries libraries) throws IOException {
+ repackage(this.source, libraries);
+ }
+
+ /**
+ * Repackage to the given destination so that it can be run using '{@literal java -jar}
+ * '
+ * @param destination the destination file (may be the same as the source)
+ * @param libraries the libraries required to run the archive
+ * @throws IOException
+ */
+ public void repackage(File destination, Libraries libraries) throws IOException {
+ if (destination == null || destination.isDirectory()) {
+ throw new IllegalArgumentException("Invalid destination");
+ }
+ if (libraries == null) {
+ throw new IllegalArgumentException("Libraries must not be null");
+ }
+ destination = destination.getAbsoluteFile();
+ File workingSource = this.source;
+ if (this.source.equals(destination)) {
+ workingSource = new File(this.source.getParentFile(), this.source.getName()
+ + ".original");
+ workingSource.delete();
+ renameFile(this.source, workingSource);
+ }
+ destination.delete();
+ try {
+ JarFile jarFileSource = new JarFile(workingSource);
+ try {
+ repackage(jarFileSource, destination, libraries);
+ }
+ finally {
+ jarFileSource.close();
+ }
+ }
+ finally {
+ if (!this.backupSource && !this.source.equals(workingSource)) {
+ deleteFile(workingSource);
+ }
+ }
+ }
+
+ private void repackage(JarFile sourceJar, File destination, Libraries libraries)
+ throws IOException {
+ final JarWriter writer = new JarWriter(destination);
+ try {
+ writer.writeManifest(buildManifest(sourceJar));
+ writer.writeEntries(sourceJar);
+ libraries.doWithLibraries(new LibraryCallback() {
+
+ @Override
+ public void library(File file, LibraryScope scope) throws IOException {
+ String destination = Repackager.this.layout.getLibraryDestination(
+ file.getName(), scope);
+ if (destination != null) {
+ writer.writeNestedLibrary(destination, file);
+ }
+ }
+ });
+ writer.writeLoaderClasses();
+ }
+ finally {
+ writer.close();
+ }
+ }
+
+ private Manifest buildManifest(JarFile source) throws IOException {
+ Manifest manifest = source.getManifest();
+ if (manifest == null) {
+ manifest = new Manifest();
+ manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
+ }
+ manifest = new Manifest(manifest);
+ String startClass = this.mainClass;
+ if (startClass == null) {
+ startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
+ }
+ if (startClass == null) {
+ startClass = MainClassFinder.findMainClass(source,
+ this.layout.getClassesLocation());
+ }
+ if (startClass == null) {
+ throw new IllegalStateException("Unable to find main class");
+ }
+ manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE,
+ this.layout.getLauncherClassName());
+ manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass);
+ return manifest;
+ }
+
+ private void renameFile(File file, File dest) {
+ if (!file.renameTo(dest)) {
+ throw new IllegalStateException("Unable to rename '" + file + "' to '" + dest
+ + "'");
+ }
+ }
+
+ private void deleteFile(File file) {
+ if (!file.delete()) {
+ throw new IllegalStateException("Unable to delete '" + file + "'");
+ }
+ }
+
+}
diff --git a/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/LayoutsTests.java b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/LayoutsTests.java
new file mode 100644
index 00000000000..c68b1d63264
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/LayoutsTests.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.File;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Tests for {@link Layouts}.
+ *
+ * @author Phillip Webb
+ */
+public class LayoutsTests {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void jarFile() throws Exception {
+ assertThat(Layouts.forFile(new File("test.jar")), instanceOf(Layouts.Jar.class));
+ assertThat(Layouts.forFile(new File("test.JAR")), instanceOf(Layouts.Jar.class));
+ assertThat(Layouts.forFile(new File("test.jAr")), instanceOf(Layouts.Jar.class));
+ assertThat(Layouts.forFile(new File("te.st.jar")), instanceOf(Layouts.Jar.class));
+ }
+
+ @Test
+ public void warFile() throws Exception {
+ assertThat(Layouts.forFile(new File("test.war")), instanceOf(Layouts.War.class));
+ assertThat(Layouts.forFile(new File("test.WAR")), instanceOf(Layouts.War.class));
+ assertThat(Layouts.forFile(new File("test.wAr")), instanceOf(Layouts.War.class));
+ assertThat(Layouts.forFile(new File("te.st.war")), instanceOf(Layouts.War.class));
+ }
+
+ @Test
+ public void unknownFile() throws Exception {
+ this.thrown.equals(IllegalStateException.class);
+ this.thrown.expectMessage("Unable to deduce layout for 'test.txt'");
+ Layouts.forFile(new File("test.txt"));
+ }
+
+ @Test
+ public void jarLayout() throws Exception {
+ Layout layout = new Layouts.Jar();
+ assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE),
+ equalTo("lib/"));
+ assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED),
+ equalTo("lib/"));
+ assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME),
+ equalTo("lib/"));
+ }
+
+ @Test
+ public void warLayout() throws Exception {
+ Layout layout = new Layouts.War();
+ assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE),
+ equalTo("WEB-INF/lib/"));
+ assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED),
+ equalTo("WEB-INF/lib-provided/"));
+ assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME),
+ equalTo("WEB-INF/lib/"));
+ }
+
+}
diff --git a/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/MainClassFinderTests.java b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/MainClassFinderTests.java
new file mode 100644
index 00000000000..a1d881acb59
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/MainClassFinderTests.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.IOException;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.springframework.boot.launcher.tools.sample.ClassWithMainMethod;
+import org.springframework.boot.launcher.tools.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();
+
+ private TestJarFile testJarFile;
+
+ @Before
+ public void setup() throws IOException {
+ this.testJarFile = new TestJarFile(this.temporaryFolder);
+ }
+
+ @Test
+ public void findMainClassInJar() throws Exception {
+ this.testJarFile.addClass("B.class", ClassWithMainMethod.class);
+ this.testJarFile.addClass("A.class", ClassWithoutMainMethod.class);
+ String actual = MainClassFinder.findMainClass(this.testJarFile.getJarFile(), "");
+ assertThat(actual, equalTo("B"));
+ }
+
+ @Test
+ public void findMainClassInJarSubFolder() throws Exception {
+ this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
+ this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
+ this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
+ String actual = MainClassFinder.findMainClass(this.testJarFile.getJarFile(), "");
+ assertThat(actual, equalTo("a.b.c.D"));
+ }
+
+ @Test
+ public void usesBreadthFirstJarSearch() throws Exception {
+ this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
+ this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
+ String actual = MainClassFinder.findMainClass(this.testJarFile.getJarFile(), "");
+ assertThat(actual, equalTo("a.B"));
+ }
+
+ @Test
+ public void findMainClassInJarSubLocation() throws Exception {
+ this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
+ this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
+ String actual = MainClassFinder
+ .findMainClass(this.testJarFile.getJarFile(), "a/");
+ assertThat(actual, equalTo("B"));
+
+ }
+
+ @Test
+ public void findMainClassInFolder() throws Exception {
+ this.testJarFile.addClass("B.class", ClassWithMainMethod.class);
+ this.testJarFile.addClass("A.class", ClassWithoutMainMethod.class);
+ String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource());
+ assertThat(actual, equalTo("B"));
+ }
+
+ @Test
+ public void findMainClassInSubFolder() throws Exception {
+ this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class);
+ this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class);
+ this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class);
+ String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource());
+ assertThat(actual, equalTo("a.b.c.D"));
+ }
+
+ @Test
+ public void usesBreadthFirstFolderSearch() throws Exception {
+ this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class);
+ this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class);
+ String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource());
+ assertThat(actual, equalTo("a.B"));
+ }
+
+}
diff --git a/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/RepackagerTests.java b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/RepackagerTests.java
new file mode 100644
index 00000000000..9fad583fb92
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/RepackagerTests.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.springframework.boot.launcher.tools.sample.ClassWithMainMethod;
+import org.springframework.boot.launcher.tools.sample.ClassWithoutMainMethod;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link Repackager}.
+ *
+ * @author Phillip Webb
+ */
+public class RepackagerTests {
+
+ private static final Libraries NO_LIBRARIES = new Libraries() {
+ @Override
+ public void doWithLibraries(LibraryCallback callback) throws IOException {
+ }
+ };
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private TestJarFile testJarFile;
+
+ @Before
+ public void setup() throws IOException {
+ this.testJarFile = new TestJarFile(this.temporaryFolder);
+ }
+
+ @Test
+ public void nullSource() throws Exception {
+ this.thrown.expect(IllegalArgumentException.class);
+ new Repackager(null);
+ }
+
+ @Test
+ public void missingSource() throws Exception {
+ this.thrown.expect(IllegalArgumentException.class);
+ new Repackager(new File("missing"));
+ }
+
+ @Test
+ public void directorySource() throws Exception {
+ this.thrown.expect(IllegalArgumentException.class);
+ new Repackager(this.temporaryFolder.getRoot());
+ }
+
+ @Test
+ public void specificMainClass() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithoutMainMethod.class);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ repackager.setMainClass("a.b.C");
+ repackager.repackage(NO_LIBRARIES);
+ Manifest actualManifest = getManifest(file);
+ assertThat(actualManifest.getMainAttributes().getValue("Main-Class"),
+ equalTo("org.springframework.boot.loader.JarLauncher"));
+ assertThat(actualManifest.getMainAttributes().getValue("Start-Class"),
+ equalTo("a.b.C"));
+ assertThat(hasLauncherClasses(file), equalTo(true));
+ }
+
+ @Test
+ public void mainClassFromManifest() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithoutMainMethod.class);
+ Manifest manifest = new Manifest();
+ manifest = new Manifest();
+ manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
+ manifest.getMainAttributes().putValue("Main-Class", "a.b.C");
+ this.testJarFile.addManifest(manifest);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ repackager.repackage(NO_LIBRARIES);
+ Manifest actualManifest = getManifest(file);
+ assertThat(actualManifest.getMainAttributes().getValue("Main-Class"),
+ equalTo("org.springframework.boot.loader.JarLauncher"));
+ assertThat(actualManifest.getMainAttributes().getValue("Start-Class"),
+ equalTo("a.b.C"));
+ assertThat(hasLauncherClasses(file), equalTo(true));
+ }
+
+ @Test
+ public void mainClassFound() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ repackager.repackage(NO_LIBRARIES);
+ Manifest actualManifest = getManifest(file);
+ assertThat(actualManifest.getMainAttributes().getValue("Main-Class"),
+ equalTo("org.springframework.boot.loader.JarLauncher"));
+ assertThat(actualManifest.getMainAttributes().getValue("Start-Class"),
+ equalTo("a.b.C"));
+ assertThat(hasLauncherClasses(file), equalTo(true));
+ }
+
+ @Test
+ public void noMainClass() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithoutMainMethod.class);
+ this.thrown.expect(IllegalStateException.class);
+ this.thrown.expectMessage("Unable to find main class");
+ new Repackager(this.testJarFile.getFile()).repackage(NO_LIBRARIES);
+ }
+
+ @Test
+ public void sameSourceAndDestinationWithBackup() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ repackager.repackage(NO_LIBRARIES);
+ assertThat(new File(file.getParent(), file.getName() + ".original").exists(),
+ equalTo(true));
+ assertThat(hasLauncherClasses(file), equalTo(true));
+ }
+
+ @Test
+ public void sameSourceAndDestinationWithoutBackup() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ repackager.setBackupSource(false);
+ repackager.repackage(NO_LIBRARIES);
+ assertThat(new File(file.getParent(), file.getName() + ".original").exists(),
+ equalTo(false));
+ assertThat(hasLauncherClasses(file), equalTo(true));
+ }
+
+ @Test
+ public void differentDestination() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ File source = this.testJarFile.getFile();
+ File dest = this.temporaryFolder.newFile("different.jar");
+ Repackager repackager = new Repackager(source);
+ repackager.repackage(dest, NO_LIBRARIES);
+ assertThat(new File(source.getParent(), source.getName() + ".original").exists(),
+ equalTo(false));
+ assertThat(hasLauncherClasses(source), equalTo(false));
+ assertThat(hasLauncherClasses(dest), equalTo(true));
+
+ }
+
+ @Test
+ public void nullDestination() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ Repackager repackager = new Repackager(this.testJarFile.getFile());
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Invalid destination");
+ repackager.repackage(null, NO_LIBRARIES);
+ }
+
+ @Test
+ public void destinationIsDirectory() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ Repackager repackager = new Repackager(this.testJarFile.getFile());
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Invalid destination");
+ repackager.repackage(this.temporaryFolder.getRoot(), NO_LIBRARIES);
+ }
+
+ @Test
+ public void overwriteDestination() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ Repackager repackager = new Repackager(this.testJarFile.getFile());
+ File dest = this.temporaryFolder.newFile("dest.jar");
+ dest.createNewFile();
+ repackager.repackage(dest, NO_LIBRARIES);
+ assertThat(hasLauncherClasses(dest), equalTo(true));
+ }
+
+ @Test
+ public void nullLibraries() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Libraries must not be null");
+ repackager.repackage(file, null);
+ }
+
+ @Test
+ public void libraries() throws Exception {
+ TestJarFile libJar = new TestJarFile(this.temporaryFolder);
+ libJar.addClass("a.b.C.class", ClassWithoutMainMethod.class);
+ final File libJarFile = libJar.getFile();
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ repackager.repackage(new Libraries() {
+ @Override
+ public void doWithLibraries(LibraryCallback callback) throws IOException {
+ callback.library(libJarFile, LibraryScope.COMPILE);
+ }
+ });
+ assertThat(hasEntry(file, "lib/" + libJarFile.getName()), equalTo(true));
+ }
+
+ @Test
+ public void customLayout() throws Exception {
+ TestJarFile libJar = new TestJarFile(this.temporaryFolder);
+ libJar.addClass("a.b.C.class", ClassWithoutMainMethod.class);
+ final File libJarFile = libJar.getFile();
+ this.testJarFile.addClass("a.b.C.class", ClassWithMainMethod.class);
+ File file = this.testJarFile.getFile();
+ Repackager repackager = new Repackager(file);
+ Layout layout = mock(Layout.class);
+ final LibraryScope scope = mock(LibraryScope.class);
+ given(layout.getLauncherClassName()).willReturn("testLauncher");
+ given(layout.getLibraryDestination(anyString(), eq(scope))).willReturn("test/");
+ repackager.setLayout(layout);
+ repackager.repackage(new Libraries() {
+ @Override
+ public void doWithLibraries(LibraryCallback callback) throws IOException {
+ callback.library(libJarFile, scope);
+ }
+ });
+ assertThat(hasEntry(file, "test/" + libJarFile.getName()), equalTo(true));
+ assertThat(getManifest(file).getMainAttributes().getValue("Main-Class"),
+ equalTo("testLauncher"));
+ }
+
+ @Test
+ public void nullCustomLayout() throws Exception {
+ this.testJarFile.addClass("a.b.C.class", ClassWithoutMainMethod.class);
+ Repackager repackager = new Repackager(this.testJarFile.getFile());
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Layout must not be null");
+ repackager.setLayout(null);
+ }
+
+ private boolean hasLauncherClasses(File file) throws IOException {
+ return hasEntry(file, "org/springframework/boot/")
+ && hasEntry(file, "org/springframework/boot/loader/JarLauncher.class");
+ }
+
+ private boolean hasEntry(File file, String name) throws IOException {
+ JarFile jarFile = new JarFile(file);
+ try {
+ return jarFile.getEntry(name) != null;
+ }
+ finally {
+ jarFile.close();
+ }
+ }
+
+ private Manifest getManifest(File file) throws IOException {
+ JarFile jarFile = new JarFile(file);
+ try {
+ return jarFile.getManifest();
+ }
+ finally {
+ jarFile.close();
+ }
+ }
+
+}
diff --git a/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/TestJarFile.java b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/TestJarFile.java
new file mode 100644
index 00000000000..a19f908cf29
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/TestJarFile.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import org.junit.rules.TemporaryFolder;
+import org.zeroturnaround.zip.ZipUtil;
+
+/**
+ * @author Phillip Webb
+ */
+public class TestJarFile {
+
+ private final byte[] buffer = new byte[4096];
+
+ private TemporaryFolder temporaryFolder;
+
+ private File jarSource;
+
+ public TestJarFile(TemporaryFolder temporaryFolder) throws IOException {
+ this.temporaryFolder = temporaryFolder;
+ this.jarSource = temporaryFolder.newFolder();
+ }
+
+ public void addClass(String filename, Class> classToCopy) throws IOException {
+ String[] paths = filename.split("\\/");
+ File file = this.jarSource;
+ 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 {
+ copy(inputStream, outputStream);
+ }
+ finally {
+ outputStream.close();
+ }
+ }
+
+ public void addManifest(Manifest manifest) throws IOException {
+ File manifestFile = new File(this.jarSource, "META-INF/MANIFEST.MF");
+ manifestFile.getParentFile().mkdirs();
+ OutputStream outputStream = new FileOutputStream(manifestFile);
+ try {
+ manifest.write(outputStream);
+ }
+ finally {
+ outputStream.close();
+ }
+ }
+
+ private void copy(InputStream in, OutputStream out) throws IOException {
+ int bytesRead = -1;
+ while ((bytesRead = in.read(this.buffer)) != -1) {
+ out.write(this.buffer, 0, bytesRead);
+ }
+ }
+
+ public JarFile getJarFile() throws IOException {
+ return new JarFile(getFile());
+ }
+
+ public File getJarSource() {
+ return this.jarSource;
+ }
+
+ public File getFile() throws IOException {
+ File file = this.temporaryFolder.newFile();
+ file = new File(file.getParent(), file.getName() + ".jar");
+ ZipUtil.pack(this.jarSource, file);
+ return file;
+ }
+
+}
diff --git a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableJarHelper.java b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithMainMethod.java
similarity index 50%
rename from spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableJarHelper.java
rename to spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithMainMethod.java
index 359920a1657..0cafd06ece8 100644
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableJarHelper.java
+++ b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithMainMethod.java
@@ -14,35 +14,16 @@
* limitations under the License.
*/
-package org.springframework.boot.maven;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.apache.maven.artifact.Artifact;
+package org.springframework.boot.launcher.tools.sample;
/**
- * Help build an executable JAR file.
+ * Sample class with a main method.
*
* @author Phillip Webb
- * @author Dave Syer
*/
-public class ExecutableJarHelper implements ArchiveHelper {
+public class ClassWithMainMethod {
- private static final Set LIB_SCOPES = new HashSet(Arrays.asList(
- "compile", "runtime", "provided"));
-
- @Override
- public String getArtifactDestination(Artifact artifact) {
- if (LIB_SCOPES.contains(artifact.getScope())) {
- return "lib/";
- }
- return null;
+ public static void main(String[] args) {
}
- @Override
- public String getLauncherClass() {
- return "org.springframework.boot.loader.JarLauncher";
- }
}
diff --git a/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithoutMainMethod.java b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithoutMainMethod.java
new file mode 100644
index 00000000000..aef26e24174
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithoutMainMethod.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.launcher.tools.sample;
+
+/**
+ * Sample class without a main method.
+ *
+ * @author Phillip Webb
+ */
+public class ClassWithoutMainMethod {
+
+}
diff --git a/spring-boot-loader-tools/src/test/resources/META-INF/MANIFEST.MF b/spring-boot-loader-tools/src/test/resources/META-INF/MANIFEST.MF
new file mode 100644
index 00000000000..1d3f38cc976
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,11 @@
+Manifest-Version: 1.0
+Implementation-Title: spring-boot-sample-tomcat
+Implementation-Version: 0.5.0.BUILD-SNAPSHOT
+Implementation-Vendor-Id: org.springframework.boot
+Built-By: pwebb
+Build-Jdk: 1.7.0_17
+Specification-Title: spring-boot-sample-tomcat
+Created-By: Apache Maven 3.0.5
+Specification-Version: 0.5.0.BUILD-SNAPSHOT
+Archiver-Version: Plexus Archiver
+
diff --git a/spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.properties b/spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.properties
new file mode 100644
index 00000000000..a0a9d0e3afa
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.properties
@@ -0,0 +1,7 @@
+#Generated by Maven Integration for Eclipse
+#Fri Jul 26 13:40:41 PDT 2013
+version=0.5.0.BUILD-SNAPSHOT
+groupId=org.springframework.boot
+m2e.projectName=spring-boot-sample-tomcat
+m2e.projectLocation=/Users/pwebb/projects/spring/spring-bootstrap/code/spring-boot-samples/spring-boot-sample-tomcat
+artifactId=spring-boot-sample-tomcat
diff --git a/spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.xml b/spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.xml
new file mode 100644
index 00000000000..8ae6fca8913
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-samples
+ 0.5.0.BUILD-SNAPSHOT
+
+ spring-boot-sample-tomcat
+ jar
+
+ ${basedir}/../..
+
+
+
+ ${project.groupId}
+ spring-boot-up
+ ${project.version}
+
+
+ ${project.groupId}
+ spring-boot-up-tomcat
+ ${project.version}
+
+
+ org.springframework
+ spring-webmvc
+
+
+
+
+
+ ${project.groupId}
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/SampleTomcatApplication.class b/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/SampleTomcatApplication.class
new file mode 100644
index 00000000000..74bef72f25f
Binary files /dev/null and b/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/SampleTomcatApplication.class differ
diff --git a/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/service/HelloWorldService.class b/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/service/HelloWorldService.class
new file mode 100644
index 00000000000..329bb699479
Binary files /dev/null and b/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/service/HelloWorldService.class differ
diff --git a/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/web/SampleController.class b/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/web/SampleController.class
new file mode 100644
index 00000000000..e72a966b025
Binary files /dev/null and b/spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/web/SampleController.class differ
diff --git a/spring-boot-loader-tools/src/test/resources/original.jar b/spring-boot-loader-tools/src/test/resources/original.jar
new file mode 100644
index 00000000000..1dadfc4ffd0
Binary files /dev/null and b/spring-boot-loader-tools/src/test/resources/original.jar differ
diff --git a/spring-boot-loader-tools/src/test/resources/original2.jar b/spring-boot-loader-tools/src/test/resources/original2.jar
new file mode 100644
index 00000000000..7817fcee2b7
Binary files /dev/null and b/spring-boot-loader-tools/src/test/resources/original2.jar differ
diff --git a/spring-boot-loader-tools/src/test/resources/public/test.css b/spring-boot-loader-tools/src/test/resources/public/test.css
new file mode 100644
index 00000000000..a6e538f4a48
--- /dev/null
+++ b/spring-boot-loader-tools/src/test/resources/public/test.css
@@ -0,0 +1 @@
+p.{}
diff --git a/spring-boot-maven-plugin/pom.xml b/spring-boot-maven-plugin/pom.xml
index bd12c7d0356..57ecb5beec8 100644
--- a/spring-boot-maven-plugin/pom.xml
+++ b/spring-boot-maven-plugin/pom.xml
@@ -16,7 +16,7 @@
${project.groupId}
- spring-boot-loader
+ spring-boot-loader-tools
${project.version}
diff --git a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractExecutableArchiveMojo.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractExecutableArchiveMojo.java
deleted file mode 100644
index 8b8ab65afcb..00000000000
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractExecutableArchiveMojo.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 2012-2013 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.boot.maven;
-
-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-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java
new file mode 100644
index 00000000000..9e49f51b079
--- /dev/null
+++ b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.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.boot.maven;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.artifact.Artifact;
+import org.springframework.boot.launcher.tools.Libraries;
+import org.springframework.boot.launcher.tools.LibraryCallback;
+import org.springframework.boot.launcher.tools.LibraryScope;
+
+/**
+ * {@link Libraries} backed by Maven {@link Artifact}s
+ *
+ * @author Phillip Webb
+ */
+public class ArtifactsLibraries implements Libraries {
+
+ private static final Map SCOPES;
+ static {
+ Map scopes = new HashMap();
+ scopes.put(Artifact.SCOPE_COMPILE, LibraryScope.COMPILE);
+ scopes.put(Artifact.SCOPE_RUNTIME, LibraryScope.RUNTIME);
+ scopes.put(Artifact.SCOPE_PROVIDED, LibraryScope.PROVIDED);
+ SCOPES = Collections.unmodifiableMap(scopes);
+ }
+
+ private final Set artifacts;
+
+ public ArtifactsLibraries(Set artifacts) {
+ this.artifacts = artifacts;
+ }
+
+ @Override
+ public void doWithLibraries(LibraryCallback callback) throws IOException {
+ for (Artifact artifact : this.artifacts) {
+ LibraryScope scope = SCOPES.get(artifact.getScope());
+ if (scope != null && artifact.getFile() != null) {
+ callback.library(artifact.getFile(), scope);
+ }
+ }
+ }
+}
diff --git a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableArchiveMojo.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableArchiveMojo.java
deleted file mode 100644
index 0e7e4c7175c..00000000000
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableArchiveMojo.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright 2012-2013 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.boot.maven;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.jar.Attributes;
-import java.util.jar.Attributes.Name;
-
-import org.apache.maven.archiver.MavenArchiver;
-import org.apache.maven.artifact.Artifact;
-import org.apache.maven.execution.MavenSession;
-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.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.MavenProjectHelper;
-import org.codehaus.plexus.archiver.Archiver;
-import org.codehaus.plexus.archiver.jar.JarArchiver;
-import org.codehaus.plexus.archiver.jar.Manifest;
-import org.codehaus.plexus.archiver.zip.ZipEntry;
-import org.codehaus.plexus.archiver.zip.ZipFile;
-import org.codehaus.plexus.archiver.zip.ZipResource;
-import org.codehaus.plexus.util.IOUtil;
-import org.sonatype.aether.RepositorySystem;
-import org.sonatype.aether.RepositorySystemSession;
-import org.sonatype.aether.repository.RemoteRepository;
-import org.sonatype.aether.resolution.ArtifactDescriptorRequest;
-import org.sonatype.aether.resolution.ArtifactDescriptorResult;
-import org.sonatype.aether.resolution.ArtifactRequest;
-import org.sonatype.aether.resolution.ArtifactResult;
-import org.sonatype.aether.util.artifact.DefaultArtifact;
-
-/**
- * MOJO that can can be used to repackage existing JAR and WAR archives so that they can
- * be executed from the command line using {@literal java -jar}.
- *
- * @author Phillip Webb
- * @author Dave Syer
- */
-@Mojo(name = "package", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
-public class ExecutableArchiveMojo extends AbstractExecutableArchiveMojo {
-
- 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 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;
-
- /**
- * 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;
-
- /**
- * Whether creating the archive should be forced.
- */
- @Parameter(property = "archive.forceCreation", defaultValue = "true")
- private boolean forceCreation;
-
- /**
- * The current repository/network configuration of Maven.
- */
- @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
- private RepositorySystemSession repositorySystemSession;
-
- @Override
- public void execute() throws MojoExecutionException, MojoFailureException {
- File archiveFile = createArchive();
- if (this.classifier == null || this.classifier.isEmpty()) {
- getProject().getArtifact().setFile(archiveFile);
- }
- else {
- getLog().info(
- "Attaching archive: " + archiveFile + ", with classifier: "
- + this.classifier);
- this.projectHelper.attachArtifact(getProject(), getType(), this.classifier,
- archiveFile);
- }
- }
-
- private File createArchive() throws MojoExecutionException {
- File archiveFile = getTargetFile();
- MavenArchiver archiver = new MavenArchiver();
-
- archiver.setArchiver(this.jarArchiver);
- archiver.setOutputFile(archiveFile);
- archiver.getArchiver().setRecompressAddedZips(false);
-
- try {
- getLog().info("Modifying archive: " + archiveFile);
- Manifest manifest = copyContent(archiver, getProject().getArtifact()
- .getFile());
- customizeArchiveConfiguration(manifest);
- addLibs(archiver);
- ZipFile zipFile = addLauncherClasses(archiver);
- try {
- archiver.createArchive(this.session, getProject(),
- getArchiveConfiguration());
- return archiveFile;
- }
- finally {
- zipFile.close();
- }
- }
- catch (Exception ex) {
- throw new MojoExecutionException("Error assembling archive", ex);
- }
- }
-
- private Manifest copyContent(MavenArchiver archiver, File file) throws IOException {
-
- FileInputStream input = new FileInputStream(file);
- File original = new File(this.outputDirectory, "original.jar");
- FileOutputStream output = new FileOutputStream(original);
- IOUtil.copy(input, output, 2048);
- input.close();
- output.close();
-
- Manifest manifest = new Manifest();
- ZipFile zipFile = new ZipFile(original);
- Enumeration extends ZipEntry> entries = zipFile.getEntries();
- while (entries.hasMoreElements()) {
- ZipEntry entry = entries.nextElement();
- if (!entry.isDirectory()) {
- ZipResource zipResource = new ZipResource(zipFile, entry);
- getLog().debug("Copying resource: " + entry.getName());
- if (!entry.getName().toUpperCase().equals("META-INF/MANIFEST.MF")) {
- archiver.getArchiver().addResource(zipResource, entry.getName(), -1);
- }
- else {
- getLog().info("Found existing manifest");
- manifest = new Manifest(zipResource.getContents());
- }
- }
- }
-
- return manifest;
-
- }
-
- 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(Manifest manifest)
- throws MojoExecutionException {
- getArchiveConfiguration().setForced(this.forceCreation);
-
- Attributes attributes = manifest.getMainAttributes();
- for (Object name : attributes.keySet()) {
- String value = attributes.getValue((Name) name);
- getLog().debug("Existing manifest entry: " + name + "=" + value);
- getArchiveConfiguration().addManifestEntry(name.toString(), value);
- }
-
- String startClass = getStartClass();
- getArchiveConfiguration().addManifestEntry(MAIN_CLASS_ATTRIBUTE,
- getArchiveHelper().getLauncherClass());
- getArchiveConfiguration().addManifestEntry(START_CLASS_ATTRIBUTE, startClass);
- }
-
- private void addLibs(MavenArchiver archiver) throws MojoExecutionException {
- getLog().info("Adding dependencies");
- ArchiveHelper archiveHelper = getArchiveHelper();
- for (Artifact artifact : getProject().getArtifacts()) {
- if (artifact.getFile() != null) {
- String dir = archiveHelper.getArtifactDestination(artifact);
- if (dir != null) {
- getLog().debug("Adding dependency: " + artifact);
- archiver.getArchiver().addFile(artifact.getFile(),
- dir + artifact.getFile().getName());
- }
- }
- }
- }
-
- private ZipFile addLauncherClasses(MavenArchiver archiver)
- throws MojoExecutionException {
- getLog().info("Adding launcher classes");
- try {
- List repositories = new ArrayList();
- repositories.addAll(getProject().getRemotePluginRepositories());
- repositories.addAll(getProject().getRemoteProjectRepositories());
-
- String version = getClass().getPackage().getImplementationVersion();
- DefaultArtifact artifact = new DefaultArtifact(
- "org.springframework.boot:spring-boot-loader:" + version);
- ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(
- artifact, repositories, "plugin");
- ArtifactDescriptorResult descriptorResult = this.repositorySystem
- .readArtifactDescriptor(this.repositorySystemSession,
- descriptorRequest);
-
- ArtifactRequest artifactRequest = new ArtifactRequest();
- artifactRequest.setRepositories(repositories);
- artifactRequest.setArtifact(descriptorResult.getArtifact());
- ArtifactResult artifactResult = this.repositorySystem.resolveArtifact(
- this.repositorySystemSession, artifactRequest);
-
- if (artifactResult.getArtifact() == null) {
- throw new MojoExecutionException("Unable to resolve launcher classes");
- }
- return addLauncherClasses(archiver, artifactResult.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-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableWarHelper.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableWarHelper.java
deleted file mode 100644
index 713956d11fe..00000000000
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableWarHelper.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2012-2013 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.boot.maven;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.maven.artifact.Artifact;
-
-/**
- * Build an executable WAR file.
- *
- * @author Phillip Webb
- * @author Dave Syer
- */
-public class ExecutableWarHelper implements ArchiveHelper {
-
- 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);
- }
-
- @Override
- public String getArtifactDestination(Artifact artifact) {
- return SCOPE_DESTINATIONS.get(artifact.getScope());
- }
-
- @Override
- public String getLauncherClass() {
- return "org.springframework.boot.loader.WarLauncher";
- }
-}
diff --git a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/MainClassFinder.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/MainClassFinder.java
deleted file mode 100644
index bfe924acd06..00000000000
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/MainClassFinder.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright 2012-2013 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.boot.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
- */
-abstract 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-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PackageMojo.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PackageMojo.java
new file mode 100644
index 00000000000..5927625c7f9
--- /dev/null
+++ b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PackageMojo.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2012-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.maven;
+
+import java.io.File;
+import java.io.IOException;
+
+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.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.springframework.boot.launcher.tools.Libraries;
+import org.springframework.boot.launcher.tools.Repackager;
+
+/**
+ * MOJO that can can be used to repackage existing JAR and WAR archives so that they can
+ * be executed from the command line using {@literal java -jar}.
+ *
+ * @author Phillip Webb
+ * @author Dave Syer
+ */
+@Mojo(name = "package", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
+public class PackageMojo extends AbstractMojo {
+
+ /**
+ * The Maven project.
+ */
+ @Parameter(defaultValue = "${project}", readonly = true, required = true)
+ private MavenProject project;
+
+ /**
+ * Maven project helper utils.
+ */
+ @Component
+ private MavenProjectHelper projectHelper;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ File source = this.project.getArtifact().getFile();
+ File target = getTargetFile();
+ Repackager repackager = new Repackager(source);
+ repackager.setMainClass(this.mainClass);
+ Libraries libraries = new ArtifactsLibraries(this.project.getArtifacts());
+ try {
+ repackager.repackage(target, libraries);
+ }
+ catch (IOException ex) {
+ throw new MojoExecutionException(ex.getMessage(), ex);
+ }
+ if (!source.equals(target)) {
+ getLog().info(
+ "Attaching archive: " + target + ", with classifier: "
+ + this.classifier);
+ this.projectHelper.attachArtifact(this.project, this.project.getPackaging(),
+ this.classifier, target);
+ }
+ }
+
+ 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 + "."
+ + this.project.getPackaging());
+ }
+
+}
diff --git a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java
index d045453a19a..1194c0363e9 100644
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java
+++ b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java
@@ -28,15 +28,15 @@ import org.apache.maven.plugins.shade.resource.ResourceTransformer;
/**
* Extension for the Maven
- * shade plugin to allow properties files (e.g. META-INF/spring.factories
- * ) to be merged without losing any information.
+ * shade plugin to allow properties files (e.g. {@literal META-INF/spring.factories})
+ * to be merged without losing any information.
*
* @author Dave Syer
*/
public class PropertiesMergingResourceTransformer implements ResourceTransformer {
- private String resource; // Set this in pom configuration with
- // ...
+ // Set this in pom configuration with ...
+ private String resource;
private Properties data = new Properties();
diff --git a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java
index 74a2569661c..0b9869dc8e4 100644
--- a/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java
+++ b/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java
@@ -17,6 +17,7 @@
package org.springframework.boot.maven;
import java.io.File;
+import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
@@ -26,12 +27,15 @@ import java.util.List;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Resource;
+import org.apache.maven.plugin.AbstractMojo;
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;
+import org.apache.maven.project.MavenProject;
+import org.springframework.boot.launcher.tools.MainClassFinder;
/**
* MOJO that can be used to run a executable archive application directly from Maven.
@@ -39,7 +43,13 @@ import org.apache.maven.plugins.annotations.ResolutionScope;
* @author Phillip Webb
*/
@Mojo(name = "run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, requiresDependencyResolution = ResolutionScope.TEST)
-public class RunMojo extends AbstractExecutableArchiveMojo {
+public class RunMojo extends AbstractMojo {
+
+ /**
+ * The Maven project.
+ */
+ @Parameter(defaultValue = "${project}", readonly = true, required = true)
+ private MavenProject project;
/**
* Add maven resources to the classpath directly, this allows live in-place editing or
@@ -57,12 +67,26 @@ public class RunMojo extends AbstractExecutableArchiveMojo {
@Parameter(property = "run.arguments")
private String[] arguments;
+ /**
+ * 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;
+
/**
* Folders that should be added to the classpath.
*/
@Parameter
private String[] folders;
+ /**
+ * Directory containing the classes and resource files that should be packaged into
+ * the archive.
+ */
+ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
+ private File classesDirectrory;
+
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
final String startClassName = getStartClass();
@@ -75,19 +99,35 @@ public class RunMojo extends AbstractExecutableArchiveMojo {
threadGroup.rethrowUncaughtException();
}
+ private final String getStartClass() throws MojoExecutionException {
+ String mainClass = this.mainClass;
+ if (mainClass == null) {
+ try {
+ mainClass = MainClassFinder.findMainClass(this.classesDirectrory);
+ }
+ catch (IOException ex) {
+ throw new MojoExecutionException(ex.getMessage(), ex);
+ }
+ }
+ if (mainClass == null) {
+ throw new MojoExecutionException("Unable to find a suitable main class, "
+ + "please add a 'mainClass' property");
+ }
+ return mainClass;
+ }
+
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);
+ addDependencies(urls);
return urls.toArray(new URL[urls.size()]);
}
catch (MalformedURLException ex) {
@@ -105,21 +145,20 @@ public class RunMojo extends AbstractExecutableArchiveMojo {
private void addResources(List urls) throws MalformedURLException {
if (this.addResources) {
- for (Resource resource : getProject().getResources()) {
+ for (Resource resource : this.project.getResources()) {
urls.add(new File(resource.getDirectory()).toURI().toURL());
}
}
}
private void addProjectClasses(List urls) throws MalformedURLException {
- urls.add(getClassesDirectory().toURI().toURL());
+ urls.add(this.classesDirectrory.toURI().toURL());
}
- private void addDependencies(ArchiveHelper archiveHelper, List urls)
- throws MalformedURLException {
- for (Artifact artifact : getProject().getArtifacts()) {
+ private void addDependencies(List urls) throws MalformedURLException {
+ for (Artifact artifact : this.project.getArtifacts()) {
if (artifact.getFile() != null) {
- if (archiveHelper.getArtifactDestination(artifact) != null) {
+ if (!Artifact.SCOPE_TEST.equals(artifact.getScope())) {
urls.add(artifact.getFile().toURI().toURL());
}
}
diff --git a/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/MainClassFinderTests.java b/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/MainClassFinderTests.java
deleted file mode 100644
index 5ca77c23a9b..00000000000
--- a/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/MainClassFinderTests.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2012-2013 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.boot.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.boot.maven.MainClassFinder;
-import org.springframework.boot.maven.sample.ClassWithMainMethod;
-import org.springframework.boot.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;
- }
-}