From 5e4238f38a4fd785c6a04dd11f23f49fdff2c286 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 28 Jul 2013 21:48:19 -0700 Subject: [PATCH] Create spring-loader-tools project Create spring-loader-tools containing utilities that can be used with both Maven and Gradle plugings. Refactored existing Maven plugin to use the new project. Issue: #53129653 --- pom.xml | 11 + spring-boot-loader-tools/pom.xml | 78 +++++ .../boot/launcher/tools/JarWriter.java | 218 +++++++++++++ .../boot/launcher/tools/Layout.java | 47 +++ .../boot/launcher/tools/Layouts.java | 100 ++++++ .../boot/launcher/tools/Libraries.java | 35 +++ .../boot/launcher/tools/LibraryCallback.java | 25 +- .../boot/launcher/tools/LibraryScope.java | 58 ++++ .../boot/launcher/tools/MainClassFinder.java | 243 +++++++++++++++ .../boot/launcher/tools/Repackager.java | 190 ++++++++++++ .../boot/launcher/tools/LayoutsTests.java | 84 +++++ .../launcher/tools/MainClassFinderTests.java | 108 +++++++ .../boot/launcher/tools/RepackagerTests.java | 289 ++++++++++++++++++ .../boot/launcher/tools/TestJarFile.java | 98 ++++++ .../tools/sample/ClassWithMainMethod.java | 27 +- .../tools/sample/ClassWithoutMainMethod.java | 26 ++ .../src/test/resources/META-INF/MANIFEST.MF | 11 + .../spring-boot-sample-tomcat/pom.properties | 7 + .../spring-boot-sample-tomcat/pom.xml | 39 +++ .../tomcat/SampleTomcatApplication.class | Bin 0 -> 932 bytes .../tomcat/service/HelloWorldService.class | Bin 0 -> 877 bytes .../sample/tomcat/web/SampleController.class | Bin 0 -> 973 bytes .../src/test/resources/original.jar | Bin 0 -> 5234 bytes .../src/test/resources/original2.jar | Bin 0 -> 48242 bytes .../src/test/resources/public/test.css | 1 + spring-boot-maven-plugin/pom.xml | 2 +- .../maven/AbstractExecutableArchiveMojo.java | 117 ------- .../boot/maven/ArtifactsLibraries.java | 61 ++++ .../boot/maven/ExecutableArchiveMojo.java | 289 ------------------ .../boot/maven/ExecutableWarHelper.java | 51 ---- .../boot/maven/MainClassFinder.java | 158 ---------- .../boot/maven/PackageMojo.java | 115 +++++++ .../PropertiesMergingResourceTransformer.java | 8 +- .../springframework/boot/maven/RunMojo.java | 57 +++- .../boot/maven/MainClassFinderTests.java | 97 ------ 35 files changed, 1887 insertions(+), 763 deletions(-) create mode 100644 spring-boot-loader-tools/pom.xml create mode 100644 spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/JarWriter.java create mode 100644 spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layout.java create mode 100644 spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Layouts.java create mode 100644 spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Libraries.java rename spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArchiveHelper.java => spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryCallback.java (56%) create mode 100644 spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/LibraryScope.java create mode 100644 spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/MainClassFinder.java create mode 100644 spring-boot-loader-tools/src/main/java/org/springframework/boot/launcher/tools/Repackager.java create mode 100644 spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/LayoutsTests.java create mode 100644 spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/MainClassFinderTests.java create mode 100644 spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/RepackagerTests.java create mode 100644 spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/TestJarFile.java rename spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableJarHelper.java => spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithMainMethod.java (50%) create mode 100644 spring-boot-loader-tools/src/test/java/org/springframework/boot/launcher/tools/sample/ClassWithoutMainMethod.java create mode 100644 spring-boot-loader-tools/src/test/resources/META-INF/MANIFEST.MF create mode 100644 spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.properties create mode 100644 spring-boot-loader-tools/src/test/resources/META-INF/maven/org.springframework.boot/spring-boot-sample-tomcat/pom.xml create mode 100644 spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/SampleTomcatApplication.class create mode 100644 spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/service/HelloWorldService.class create mode 100644 spring-boot-loader-tools/src/test/resources/org/springframework/boot/sample/tomcat/web/SampleController.class create mode 100644 spring-boot-loader-tools/src/test/resources/original.jar create mode 100644 spring-boot-loader-tools/src/test/resources/original2.jar create mode 100644 spring-boot-loader-tools/src/test/resources/public/test.css delete mode 100644 spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractExecutableArchiveMojo.java create mode 100644 spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java delete mode 100644 spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableArchiveMojo.java delete mode 100644 spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ExecutableWarHelper.java delete mode 100644 spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/MainClassFinder.java create mode 100644 spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PackageMojo.java delete mode 100644 spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/MainClassFinderTests.java diff --git a/pom.xml b/pom.xml index 9590322308..a5d4737889 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 0000000000..f54981aaca --- /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 0000000000..65c37ff7aa --- /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 0000000000..d52db09343 --- /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 0000000000..f6c1693da6 --- /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 0000000000..c1c2ab1372 --- /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 9e961832c3..3e621fd002 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 0000000000..9021a679d9 --- /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 0000000000..c555aa1d10 --- /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 0000000000..33201b09c8 --- /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 0000000000..c68b1d6326 --- /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 0000000000..a1d881acb5 --- /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 0000000000..9fad583fb9 --- /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 0000000000..a19f908cf2 --- /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 359920a165..0cafd06ece 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 0000000000..aef26e2417 --- /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 0000000000..1d3f38cc97 --- /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 0000000000..a0a9d0e3af --- /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 0000000000..8ae6fca891 --- /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 0000000000000000000000000000000000000000..74bef72f25fb28c872771cfa2ee1d4ee15ed8112 GIT binary patch literal 932 zcmbtS&rcIU6#k|ZwyYJfRTM>#Tj{|`Ja8!?HUv+NCN3!lPt$c+2X<#Rv$M*-ySuot1haAa)8tI{f+om)}JFUDT-i7}2> zq9}98ohee`coL%15Dd#QSCmvnM`$u^T!_zt=R#-v-Q+^1j$!3cY2}U>mU{bRhUT&P zC}S*PwT)%0FszT1mT&7~BJHV|8u8e2rbl`8tvqj<8$E0)0EdIY(|snEj{Ot60N2fo#yZNtFiy v<{-cVtq$vdNB3-o6jKG*p|0IIv=+ATl=2O_!`p2fra_Cziw`@qvomw%?AiVK>)Q_i_tDHDBkEH3#j38 zxXu-UYGWR#*;|Fc)gqR$5^A@LxQ-j#u9AezP~h&Ora7G1 za6*JloLLaq}~upT*ibgH@SfWpI01b!W;l%xnO^0q8Z}_W%F@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e72a966b025f33431d8b3509ee4c7002994d85ab GIT binary patch literal 973 zcmbtT%We}v5UlYdc8no!9^sjsk{~fs4hYGBNFpH)4oERFhn!~D9cRLNXPKF`<-Y<6 zfy99i;G+;db`(d^9wKp>a<{u{s;g)J{QdPCz%DjxC<(NT?Ms(go%TEIa&huj`Gy&0d=+0Q9^rZYP|B=q)y;P@dcz(Cr?!DjU7vM(IEG7 zZ${cuk7?&#>QwvJ0_Cmku0Z9$^r(R{ZZa);B z$K*Dqw>lWVcJ&QD3+@O!Eu6%8vSj=?BRPAuEo`vld2~dVmE+)2UZOl literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1dadfc4ffd0652da477efedc286addc1cf22857f GIT binary patch literal 5234 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 H!e9Rd>RyYG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7817fcee2b76d9b0d19c7c21d800a2daffb39740 GIT binary patch literal 48242 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 ${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 8b8ab65afc..0000000000 --- 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 0000000000..9e49f51b07 --- /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 0e7e4c7175..0000000000 --- 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 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 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 713956d11f..0000000000 --- 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 bfe924acd0..0000000000 --- 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 0000000000..5927625c7f --- /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 d045453a19..1194c0363e 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 74a2569661..0b9869dc8e 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 5ca77c23a9..0000000000 --- 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; - } -}