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