diff --git a/spring-boot-tools/spring-boot-gradle-plugin/build.gradle b/spring-boot-tools/spring-boot-gradle-plugin/build.gradle index 7de2a07346..5bef3dcec6 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/build.gradle +++ b/spring-boot-tools/spring-boot-gradle-plugin/build.gradle @@ -35,6 +35,7 @@ if (effectivePomFile.file) { dependencies { compile localGroovy() compile gradleApi() + testCompile gradleTestKit() } jar { diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchive.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchive.java new file mode 100644 index 0000000000..46c8db3cd5 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchive.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +/** + * A Spring Boot "fat" archive task. + * + * @author Andy Wilkinson + */ +public interface BootArchive extends Task { + + /** + * Returns the main class of the application. + * + * @return the main class + */ + @Input + @Optional + String getMainClass(); + + /** + * Sets the main class of the application. + * + * @param mainClass the main class of the application + */ + void setMainClass(String mainClass); + + /** + * Adds Ant-style patterns that identify files that must be unpacked from the archive + * when it is launched. + * + * @param patterns the patterns + */ + void requiresUnpack(String... patterns); + + /** + * Adds a spec that identifies files that must be unpacked from the archive when it is + * launched. + * + * @param spec the spec + */ + void requiresUnpack(Spec spec); + + /** + * Returns the {@link LaunchScriptConfiguration} that will control the script, if any, + * that is prepended to the archive. + * + * @return the launch script configuration + */ + @Input + LaunchScriptConfiguration getLaunchScript(); + + /** + * Applies the given {@code action} to the {@link LaunchScriptConfiguration} of this + * archive. + * + * @param action the action to apply + */ + void launchScript(Action action); + + /** + * Returns the classpath that will be included in the archive. + * + * @return the classpath + */ + @Optional + @Classpath + FileCollection getClasspath(); + + /** + * Adds files to the classpath to include in the archive. The given {@code classpath} + * are evaluated as per {@link Project#files(Object...)}. + * + * @param classpath the additions to the classpath + */ + void classpath(Object... classpath); + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java new file mode 100644 index 0000000000..2dbc1040a9 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.java.archives.Attributes; +import org.gradle.api.specs.Spec; +import org.gradle.api.specs.Specs; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.util.PatternSet; + +/** + * Support class for implementations of {@link BootArchive}. + * + * @author Andy Wilkinson + */ +class BootArchiveSupport { + + private final PatternSet requiresUnpack = new PatternSet(); + + private final MainClassSupplier mainClassSupplier; + + private final Set storedPathPrefixes; + + private String loaderMainClass; + + private LaunchScriptConfiguration launchScript = new LaunchScriptConfiguration(); + + BootArchiveSupport(MainClassSupplier mainClassSupplier, + String... storedPathPrefixes) { + this.mainClassSupplier = mainClassSupplier; + this.storedPathPrefixes = new HashSet<>(Arrays.asList(storedPathPrefixes)); + this.requiresUnpack.include(Specs.satisfyNone()); + } + + void configureManifest(Jar jar) { + Attributes attributes = jar.getManifest().getAttributes(); + attributes.putIfAbsent("Main-Class", this.loaderMainClass); + attributes.putIfAbsent("Start-Class", this.mainClassSupplier.get()); + } + + CopyAction createCopyAction(Jar jar) { + return new BootZipCopyAction(jar.getArchivePath(), this::requiresUnpacking, + this.launchScript, this.storedPathPrefixes); + } + + private boolean requiresUnpacking(FileTreeElement fileTreeElement) { + return this.requiresUnpack.getAsSpec().isSatisfiedBy(fileTreeElement); + } + + String getLoaderMainClass() { + return this.loaderMainClass; + } + + void setLoaderMainClass(String loaderMainClass) { + this.loaderMainClass = loaderMainClass; + } + + LaunchScriptConfiguration getLaunchScript() { + return this.launchScript; + } + + void setLaunchScript(LaunchScriptConfiguration launchScript) { + this.launchScript = launchScript; + } + + void requiresUnpack(String... patterns) { + this.requiresUnpack.include(patterns); + } + + void requiresUnpack(Spec spec) { + this.requiresUnpack.include(spec); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootJar.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootJar.java new file mode 100644 index 0000000000..3a118c8e6e --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootJar.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.File; +import java.util.Collections; +import java.util.concurrent.Callable; + +import org.gradle.api.Action; +import org.gradle.api.file.CopySpec; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.bundling.Jar; + +/** + * A custom {@link Jar} task that produces a Spring Boot executable jar. + * + * @author Andy Wilkinson + */ +public class BootJar extends Jar implements BootArchive { + + private final MainClassSupplier mainClassSupplier = new MainClassSupplier( + this::getClasspath); + + private BootArchiveSupport support = new BootArchiveSupport(this.mainClassSupplier, "BOOT-INF/lib"); + + private FileCollection classpath; + + private String mainClass; + + public BootJar() { + this.support.setLoaderMainClass("org.springframework.boot.loader.JarLauncher"); + CopySpec bootInf = getRootSpec().addChildBeforeSpec(getMainSpec()) + .into("BOOT-INF"); + bootInf.into("lib", classpathFiles(File::isFile)); + bootInf.into("classes", classpathFiles(File::isDirectory)); + } + + private Action classpathFiles(Spec filter) { + return (copySpec) -> { + copySpec.from((Callable>) () -> { + return this.classpath == null ? Collections.emptyList() + : this.classpath.filter(filter); + }); + }; + } + + @Override + public void copy() { + this.support.configureManifest(this); + super.copy(); + } + + @Override + protected CopyAction createCopyAction() { + return this.support.createCopyAction(this); + } + + @Override + public String getMainClass() { + return this.mainClass; + } + + @Override + public void setMainClass(String mainClass) { + this.mainClass = mainClass; + this.mainClassSupplier.setMainClass(mainClass); + } + + @Override + public void requiresUnpack(String... patterns) { + this.support.requiresUnpack(patterns); + } + + @Override + public void requiresUnpack(Spec spec) { + this.support.requiresUnpack(spec); + } + + @Override + public LaunchScriptConfiguration getLaunchScript() { + return this.support.getLaunchScript(); + } + + @Override + public void launchScript(Action action) { + action.execute(getLaunchScript()); + } + + @Override + public FileCollection getClasspath() { + return this.classpath; + } + + @Override + public void classpath(Object... classpath) { + FileCollection existingClasspath = this.classpath; + this.classpath = getProject().files( + existingClasspath == null ? Collections.emptyList() : existingClasspath, + classpath); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootWar.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootWar.java new file mode 100644 index 0000000000..dddebe0777 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootWar.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.File; +import java.util.Collections; +import java.util.concurrent.Callable; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.bundling.War; + +/** + * A custom {@link War} task that produces a Spring Boot executable war. + * + * @author Andy Wilkinson + */ +public class BootWar extends War implements BootArchive { + + private final MainClassSupplier mainClassSupplier = new MainClassSupplier( + this::getClasspath); + + private final BootArchiveSupport support = new BootArchiveSupport( + this.mainClassSupplier, "WEB-INF/lib/", "WEB-INF/lib-provided"); + + private String mainClass; + + private FileCollection providedClasspath; + + public BootWar() { + this.support.setLoaderMainClass("org.springframework.boot.loader.WarLauncher"); + getWebInf().into("lib-provided", (copySpec) -> { + copySpec.from((Callable>) () -> { + return this.providedClasspath == null ? Collections.emptyList() + : this.providedClasspath; + }); + }); + } + + @Override + public void copy() { + this.support.configureManifest(this); + super.copy(); + } + + @Override + protected CopyAction createCopyAction() { + return this.support.createCopyAction(this); + } + + @Override + public String getMainClass() { + return this.mainClass; + } + + @Override + public void setMainClass(String mainClass) { + this.mainClass = mainClass; + this.mainClassSupplier.setMainClass(mainClass); + } + + @Override + public void requiresUnpack(String... patterns) { + this.support.requiresUnpack(patterns); + } + + @Override + public void requiresUnpack(Spec spec) { + this.support.requiresUnpack(spec); + } + + @Override + public LaunchScriptConfiguration getLaunchScript() { + return this.support.getLaunchScript(); + } + + @Override + public void launchScript(Action action) { + action.execute(getLaunchScript()); + } + + @Optional + public FileCollection getProvidedClasspath() { + return this.providedClasspath; + } + + /** + * Adds files to the provided classpath to include in the war. The given + * {@code classpath} are evaluated as per {@link Project#files(Object...)}. + * + * @param classpath the additions to the classpath + */ + public void providedClasspath(Object... classpath) { + FileCollection existingClasspath = this.providedClasspath; + this.providedClasspath = getProject().files( + existingClasspath == null ? Collections.emptyList() : existingClasspath, + classpath); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java new file mode 100644 index 0000000000..652c12ad6d --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java @@ -0,0 +1,243 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Set; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.gradle.api.GradleException; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.internal.file.CopyActionProcessingStreamAction; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.internal.file.copy.CopyActionProcessingStream; +import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.WorkResult; + +import org.springframework.boot.loader.tools.DefaultLaunchScript; +import org.springframework.boot.loader.tools.FileUtils; + +/** + * A {@link CopyAction} for creating a Spring Boot zip archive (typically a jar or war). + * Stores jar files without compression as required by Spring Boot's loader. + * + * @author Andy Wilkinson + */ +class BootZipCopyAction implements CopyAction { + + private final File output; + + private final Spec requiresUnpack; + + private final LaunchScriptConfiguration launchScript; + + private final Set storedPathPrefixes; + + BootZipCopyAction(File output, Spec requiresUnpack, + LaunchScriptConfiguration launchScript, Set storedPathPrefixes) { + this.output = output; + this.requiresUnpack = requiresUnpack; + this.launchScript = launchScript; + this.storedPathPrefixes = storedPathPrefixes; + } + + @Override + public WorkResult execute(CopyActionProcessingStream stream) { + ZipOutputStream zipStream; + try { + FileOutputStream fileStream = new FileOutputStream(this.output); + writeLaunchScriptIfNecessary(fileStream); + zipStream = new ZipOutputStream(fileStream); + writeLoaderClasses(zipStream); + } + catch (IOException ex) { + throw new GradleException("Failed to create " + this.output, ex); + } + try { + stream.process(new ZipStreamAction(zipStream, this.output, + this.requiresUnpack, this.storedPathPrefixes)); + } + finally { + try { + zipStream.close(); + } + catch (IOException ex) { + // Continue + } + } + return () -> { + return true; + }; + } + + private void writeLoaderClasses(ZipOutputStream out) { + + ZipEntry entry; + try (ZipInputStream in = new ZipInputStream(getClass() + .getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { + byte[] buffer = new byte[4096]; + while ((entry = in.getNextEntry()) != null) { + if (entry.getName().endsWith((".class"))) { + out.putNextEntry(entry); + int read; + while ((read = in.read(buffer)) > 0) { + out.write(buffer, 0, read); + } + out.closeEntry(); + } + } + } + catch (IOException ex) { + throw new GradleException("Failed to write loader classes", ex); + } + } + + private void writeLaunchScriptIfNecessary(FileOutputStream fileStream) { + try { + if (this.launchScript.isIncluded()) { + fileStream.write(new DefaultLaunchScript(this.launchScript.getScript(), + this.launchScript.getProperties()).toByteArray()); + } + } + catch (IOException ex) { + throw new GradleException("Failed to write launch script to " + this.output, + ex); + } + } + + private static final class ZipStreamAction + implements CopyActionProcessingStreamAction { + + private final ZipOutputStream zipStream; + + private final File output; + + private final Spec requiresUnpack; + + private final Set storedPathPrefixes; + + private ZipStreamAction(ZipOutputStream zipStream, File output, + Spec requiresUnpack, Set storedPathPrefixes) { + this.zipStream = zipStream; + this.output = output; + this.requiresUnpack = requiresUnpack; + this.storedPathPrefixes = storedPathPrefixes; + } + + @Override + public void processFile(FileCopyDetailsInternal details) { + try { + if (details.isDirectory()) { + createDirectory(details); + } + else { + createFile(details); + } + } + catch (IOException ex) { + throw new GradleException( + "Failed to add " + details + " to " + this.output, ex); + } + } + + private void createDirectory(FileCopyDetailsInternal details) throws IOException { + ZipEntry archiveEntry = new ZipEntry( + details.getRelativePath().getPathString() + '/'); + archiveEntry.setTime(details.getLastModified()); + this.zipStream.putNextEntry(archiveEntry); + this.zipStream.closeEntry(); + } + + private void createFile(FileCopyDetailsInternal details) throws IOException { + String relativePath = details.getRelativePath().getPathString(); + ZipEntry archiveEntry = new ZipEntry(relativePath); + archiveEntry.setTime(details.getLastModified()); + this.zipStream.putNextEntry(archiveEntry); + if (isStoredEntry(relativePath)) { + archiveEntry.setMethod(ZipEntry.STORED); + archiveEntry.setSize(details.getSize()); + archiveEntry.setCompressedSize(details.getSize()); + Crc32OutputStream crcStream = new Crc32OutputStream(this.zipStream); + details.copyTo(crcStream); + archiveEntry.setCrc(crcStream.getCrc()); + if (this.requiresUnpack.isSatisfiedBy(details)) { + archiveEntry.setComment( + "UNPACK:" + FileUtils.sha1Hash(details.getFile())); + } + } + else { + details.copyTo(this.zipStream); + } + this.zipStream.closeEntry(); + } + + private boolean isStoredEntry(String relativePath) { + for (String prefix : this.storedPathPrefixes) { + if (relativePath.startsWith(prefix)) { + return true; + } + } + return false; + } + + } + + /** + * A {@code FilterOutputStream} that provides a CRC-32 of the data that is written to + * it. + */ + private static final class Crc32OutputStream extends FilterOutputStream { + + private final CRC32 crc32 = new CRC32(); + + private Crc32OutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + this.crc32.update(b); + this.out.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + this.crc32.update(b); + this.out.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.crc32.update(b, off, len); + this.out.write(b, off, len); + } + + private long getCrc() { + return this.crc32.getValue(); + } + + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BundlingPluginFeatures.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BundlingPluginFeatures.java new file mode 100644 index 0000000000..f49ff1bf90 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BundlingPluginFeatures.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.util.concurrent.Callable; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.WarPlugin; +import org.gradle.api.tasks.SourceSet; + +import org.springframework.boot.gradle.PluginFeatures; + +/** + * {@link PluginFeatures} for the bundling of an application. + * + * @author Andy Wilkinson + */ +public class BundlingPluginFeatures implements PluginFeatures { + + @Override + public void apply(Project project) { + project.getPlugins().withType(JavaPlugin.class, + (javaPlugin) -> configureBootJarTask(project)); + project.getPlugins().withType(WarPlugin.class, + (warPlugin) -> configureBootWarTask(project)); + } + + private void configureBootWarTask(Project project) { + BootWar bootWar = project.getTasks().create("bootWar", BootWar.class); + bootWar.providedClasspath(providedRuntimeConfiguration(project)); + } + + private void configureBootJarTask(Project project) { + BootJar bootJar = project.getTasks().create("bootJar", BootJar.class); + bootJar.classpath((Callable) () -> { + JavaPluginConvention convention = project.getConvention() + .getPlugin(JavaPluginConvention.class); + SourceSet mainSourceSet = convention.getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + return mainSourceSet.getRuntimeClasspath(); + }); + } + + private Configuration providedRuntimeConfiguration(Project project) { + return project.getConfigurations() + .getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/LaunchScriptConfiguration.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/LaunchScriptConfiguration.java new file mode 100644 index 0000000000..f302cc0f91 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/LaunchScriptConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.loader.tools.FileUtils; + +/** + * Encapsulates the configuration of the launch script for an executable jar or war. + * + * @author Andy Wilkinson + */ +public class LaunchScriptConfiguration implements Serializable { + + private boolean included = false; + + private final Map properties = new HashMap(); + + private File script; + + public boolean isIncluded() { + return this.included; + } + + public void setIncluded(boolean included) { + this.included = included; + } + + public Map getProperties() { + return this.properties; + } + + public void properties(Map properties) { + this.properties.putAll(properties); + } + + public File getScript() { + return this.script; + } + + public void setScript(File script) { + this.script = script; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (this.included ? 1231 : 1237); + result = prime * result + + ((this.properties == null) ? 0 : this.properties.hashCode()); + result = prime * result + ((this.script == null) ? 0 : this.script.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + LaunchScriptConfiguration other = (LaunchScriptConfiguration) obj; + if (this.included != other.included) { + return false; + } + if (!this.properties.equals(other.properties)) { + return false; + } + if (this.script == null) { + if (other.script != null) { + return false; + } + } + else if (!this.script.equals(other.script)) { + return false; + } + else if (!equalContents(this.script, other.script)) { + return false; + } + return true; + } + + private boolean equalContents(File one, File two) { + try { + return FileUtils.sha1Hash(one).equals(FileUtils.sha1Hash(two)); + } + catch (IOException ex) { + return false; + } + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/MainClassSupplier.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/MainClassSupplier.java new file mode 100644 index 0000000000..1e6a76b4eb --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/MainClassSupplier.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.File; +import java.io.IOException; +import java.util.function.Supplier; + +import org.gradle.api.file.FileCollection; + +import org.springframework.boot.loader.tools.MainClassFinder; + +/** + * Supplies the main class for an application by returning a configured main class if + * available. If a main class is not available, directories in the application's classpath + * are searched. + * + * @author Andy Wilkinson + */ +class MainClassSupplier implements Supplier { + + private final Supplier classpathSupplier; + + private String mainClass; + + MainClassSupplier(Supplier classpathSupplier) { + this.classpathSupplier = classpathSupplier; + } + + @Override + public String get() { + if (this.mainClass != null) { + return this.mainClass; + } + return findMainClass(); + } + + private String findMainClass() { + FileCollection classpath = this.classpathSupplier.get(); + return classpath == null ? null + : classpath.filter(File::isDirectory).getFiles().stream() + .map(this::findMainClass).findFirst().orElse(null); + } + + private String findMainClass(File file) { + try { + return MainClassFinder.findSingleMainClass(file); + } + catch (IOException ex) { + return null; + } + } + + void setMainClass(String mainClass) { + this.mainClass = mainClass; + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java index 403a38cbbe..0879b8c3d1 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java @@ -24,8 +24,8 @@ import org.gradle.api.tasks.compile.JavaCompile; import org.springframework.boot.gradle.SpringBootPluginExtension; import org.springframework.boot.gradle.agent.AgentPluginFeatures; +import org.springframework.boot.gradle.bundling.BundlingPluginFeatures; import org.springframework.boot.gradle.dependencymanagement.DependencyManagementPluginFeatures; -import org.springframework.boot.gradle.repackage.RepackagePluginFeatures; import org.springframework.boot.gradle.run.RunPluginFeatures; /** @@ -42,7 +42,7 @@ public class SpringBootPlugin implements Plugin { project.getExtensions().create("springBoot", SpringBootPluginExtension.class, project); new AgentPluginFeatures().apply(project); - new RepackagePluginFeatures().apply(project); + new BundlingPluginFeatures().apply(project); new RunPluginFeatures().apply(project); new DependencyManagementPluginFeatures().apply(project); project.getTasks().withType(JavaCompile.class).all(new SetUtf8EncodingAction()); diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/ProjectLibraries.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/ProjectLibraries.java deleted file mode 100644 index 1ebd3a6c33..0000000000 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/ProjectLibraries.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright 2012-2017 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.gradle.repackage; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Set; - -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.artifacts.FileCollectionDependency; -import org.gradle.api.artifacts.ModuleVersionIdentifier; -import org.gradle.api.artifacts.ProjectDependency; -import org.gradle.api.artifacts.ResolvedArtifact; - -import org.springframework.boot.gradle.SpringBootPluginExtension; -import org.springframework.boot.loader.tools.Libraries; -import org.springframework.boot.loader.tools.Library; -import org.springframework.boot.loader.tools.LibraryCallback; -import org.springframework.boot.loader.tools.LibraryScope; - -/** - * Expose Gradle {@link Configuration}s as {@link Libraries}. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -class ProjectLibraries implements Libraries { - - private final Project project; - - private final SpringBootPluginExtension extension; - - private final boolean excludeDevtools; - - private final TargetConfigurationResolver targetConfigurationResolver; - - private String providedConfigurationName = "providedRuntime"; - - private String customConfigurationName = null; - - /** - * Create a new {@link ProjectLibraries} instance of the specified {@link Project}. - * @param project the gradle project - * @param extension the extension - * @param excludeDevTools whether Spring Boot Devtools should be excluded - */ - ProjectLibraries(Project project, SpringBootPluginExtension extension, - boolean excludeDevTools) { - this.project = project; - this.extension = extension; - this.excludeDevtools = excludeDevTools; - this.targetConfigurationResolver = createTargetConfigurationResolver(); - } - - private static TargetConfigurationResolver createTargetConfigurationResolver() { - try { - return new Gradle3TargetConfigurationResolver( - ProjectDependency.class.getMethod("getTargetConfiguration")); - } - catch (Exception ex) { - return new Gradle2TargetConfigurationResolver(); - } - } - - /** - * Set the name of the provided configuration. Defaults to 'providedRuntime'. - * @param providedConfigurationName the providedConfigurationName to set - */ - public void setProvidedConfigurationName(String providedConfigurationName) { - this.providedConfigurationName = providedConfigurationName; - } - - public void setCustomConfigurationName(String customConfigurationName) { - this.customConfigurationName = customConfigurationName; - } - - @Override - public void doWithLibraries(LibraryCallback callback) throws IOException { - Set custom = getLibraries(this.customConfigurationName, - LibraryScope.CUSTOM); - if (custom != null) { - libraries(custom, callback); - } - else { - Set runtime = getLibraries("runtime", LibraryScope.RUNTIME); - Set provided = getLibraries(this.providedConfigurationName, - LibraryScope.PROVIDED); - if (provided != null) { - runtime = minus(runtime, provided); - } - libraries(runtime, callback); - libraries(provided, callback); - } - } - - private Set getLibraries(String configurationName, - LibraryScope scope) { - Configuration configuration = (configurationName == null ? null - : this.project.getConfigurations().findByName(configurationName)); - if (configuration == null) { - return null; - } - Set libraries = new LinkedHashSet<>(); - for (ResolvedArtifact artifact : configuration.getResolvedConfiguration() - .getResolvedArtifacts()) { - libraries.add(new ResolvedArtifactLibrary(artifact, scope)); - } - libraries.addAll(getLibrariesForFileDependencies(configuration, scope)); - return libraries; - } - - private Set getLibrariesForFileDependencies( - Configuration configuration, LibraryScope scope) { - Set libraries = new LinkedHashSet<>(); - for (Dependency dependency : configuration.getIncoming().getDependencies()) { - if (dependency instanceof FileCollectionDependency) { - FileCollectionDependency fileDependency = (FileCollectionDependency) dependency; - for (File file : fileDependency.resolve()) { - libraries.add( - new GradleLibrary(fileDependency.getGroup(), file, scope)); - } - } - else if (dependency instanceof ProjectDependency) { - ProjectDependency projectDependency = (ProjectDependency) dependency; - libraries - .addAll(getLibrariesForFileDependencies( - this.targetConfigurationResolver - .resolveTargetConfiguration(projectDependency), - scope)); - } - } - return libraries; - } - - private Set minus(Set source, - Set toRemove) { - if (source == null || toRemove == null) { - return source; - } - Set filesToRemove = new HashSet<>(); - for (GradleLibrary library : toRemove) { - filesToRemove.add(library.getFile()); - } - Set result = new LinkedHashSet<>(); - for (GradleLibrary library : source) { - if (!filesToRemove.contains(library.getFile())) { - result.add(library); - } - } - return result; - } - - private void libraries(Set libraries, LibraryCallback callback) - throws IOException { - if (libraries != null) { - Set duplicates = getDuplicates(libraries); - for (GradleLibrary library : libraries) { - if (!isExcluded(library)) { - library.setIncludeGroupName(duplicates.contains(library.getName())); - callback.library(library); - } - } - } - } - - private boolean isExcluded(GradleLibrary library) { - if (this.excludeDevtools && isDevToolsJar(library)) { - return true; - } - return false; - } - - private boolean isDevToolsJar(GradleLibrary library) { - return "org.springframework.boot".equals(library.getGroup()) - && library.getName().startsWith("spring-boot-devtools"); - } - - private Set getDuplicates(Set libraries) { - Set duplicates = new HashSet<>(); - Set seen = new HashSet<>(); - for (GradleLibrary library : libraries) { - if (library.getFile() != null && !seen.add(library.getFile().getName())) { - duplicates.add(library.getFile().getName()); - } - } - return duplicates; - } - - private class GradleLibrary extends Library { - - private final String group; - - private boolean includeGroupName; - - GradleLibrary(String group, File file, LibraryScope scope) { - super(file, scope); - this.group = group; - } - - public void setIncludeGroupName(boolean includeGroupName) { - this.includeGroupName = includeGroupName; - } - - public String getGroup() { - return this.group; - } - - @Override - public String getName() { - String name = super.getName(); - if (this.includeGroupName && this.group != null) { - name = this.group + "-" + name; - } - return name; - } - - @Override - public int hashCode() { - return getFile().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof GradleLibrary) { - return getFile().equals(((GradleLibrary) obj).getFile()); - } - return false; - } - - @Override - public String toString() { - return getFile().getAbsolutePath(); - } - - } - - /** - * Adapts a {@link ResolvedArtifact} to a {@link Library}. - */ - private class ResolvedArtifactLibrary extends GradleLibrary { - - private final ResolvedArtifact artifact; - - ResolvedArtifactLibrary(ResolvedArtifact artifact, LibraryScope scope) { - super(artifact.getModuleVersion().getId().getGroup(), artifact.getFile(), - scope); - this.artifact = artifact; - } - - @Override - public boolean isUnpackRequired() { - if (ProjectLibraries.this.extension.getRequiresUnpack() != null) { - ModuleVersionIdentifier id = this.artifact.getModuleVersion().getId(); - return ProjectLibraries.this.extension.getRequiresUnpack() - .contains(id.getGroup() + ":" + id.getName()); - } - return false; - } - - } - - /** - * Strategy used to resolve configurations regardless of the underlying Gradle - * version. - */ - private interface TargetConfigurationResolver { - - Configuration resolveTargetConfiguration(ProjectDependency projectDependency); - - } - - /** - * {@link TargetConfigurationResolver} for Gradle 2.x. - */ - private static final class Gradle2TargetConfigurationResolver - implements TargetConfigurationResolver { - - @Override - public Configuration resolveTargetConfiguration( - ProjectDependency projectDependency) { - return projectDependency.getProjectConfiguration(); - } - - } - - /** - * {@link TargetConfigurationResolver} for Gradle 3.x. - */ - private static final class Gradle3TargetConfigurationResolver - implements TargetConfigurationResolver { - - private final Method getTargetConfiguration; - - private Gradle3TargetConfigurationResolver(Method getTargetConfiguration) { - this.getTargetConfiguration = getTargetConfiguration; - } - - @Override - public Configuration resolveTargetConfiguration( - ProjectDependency projectDependency) { - try { - String configurationName = (String) this.getTargetConfiguration - .invoke(projectDependency); - return projectDependency.getDependencyProject().getConfigurations() - .getByName(configurationName == null - ? Dependency.DEFAULT_CONFIGURATION : configurationName); - } - catch (Exception ex) { - throw new RuntimeException("Failed to get target configuration", ex); - } - } - - } - -} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java deleted file mode 100644 index f7a540874d..0000000000 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2012-2017 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.gradle.repackage; - -import java.io.File; -import java.io.IOException; - -import org.gradle.api.Action; -import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.logging.Logger; -import org.gradle.api.plugins.BasePlugin; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.tasks.TaskDependency; -import org.gradle.api.tasks.bundling.Jar; - -import org.springframework.boot.gradle.PluginFeatures; -import org.springframework.boot.gradle.SpringBootPluginExtension; -import org.springframework.boot.gradle.run.FindMainClassTask; -import org.springframework.boot.loader.tools.Library; -import org.springframework.boot.loader.tools.LibraryCallback; -import org.springframework.util.StringUtils; - -/** - * {@link PluginFeatures} to add repackage support. - * - * @author Phillip Webb - * @author Dave Syer - * @author Andy Wilkinson - */ -public class RepackagePluginFeatures implements PluginFeatures { - - /** - * The name of the repackage task. - */ - public static final String REPACKAGE_TASK_NAME = "bootRepackage"; - - @Override - public void apply(Project project) { - project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { - addRepackageTask(project); - registerRepackageTaskProperty(project); - }); - } - - private void addRepackageTask(Project project) { - RepackageTask task = project.getTasks().create(REPACKAGE_TASK_NAME, - RepackageTask.class); - task.setDescription("Repackage existing JAR and WAR " - + "archives so that they can be executed from the command " - + "line using 'java -jar'"); - task.setGroup(BasePlugin.BUILD_GROUP); - Configuration runtimeConfiguration = project.getConfigurations() - .getByName(JavaPlugin.RUNTIME_CONFIGURATION_NAME); - TaskDependency runtimeProjectDependencyJarTasks = runtimeConfiguration - .getTaskDependencyFromProjectDependency(true, JavaPlugin.JAR_TASK_NAME); - task.dependsOn( - project.getConfigurations().getByName(Dependency.ARCHIVES_CONFIGURATION) - .getAllArtifacts().getBuildDependencies(), - runtimeProjectDependencyJarTasks); - registerOutput(project, task); - ensureTaskRunsOnAssembly(project, task); - ensureMainClassHasBeenFound(project, task); - } - - private void registerOutput(Project project, final RepackageTask task) { - project.afterEvaluate(new Action() { - @Override - public void execute(Project project) { - project.getTasks().withType(Jar.class, - new RegisterInputsOutputsAction(task)); - Object withJar = task.getWithJarTask(); - if (withJar != null) { - task.dependsOn(withJar); - } - } - }); - } - - private void ensureTaskRunsOnAssembly(Project project, Task task) { - project.getTasks().getByName(BasePlugin.ASSEMBLE_TASK_NAME).dependsOn(task); - } - - private void ensureMainClassHasBeenFound(Project project, Task task) { - task.dependsOn(project.getTasks().withType(FindMainClassTask.class)); - } - - /** - * Register BootRepackage so that we can use task {@code foo(type: BootRepackage)}. - * @param project the source project - */ - private void registerRepackageTaskProperty(Project project) { - project.getExtensions().getExtraProperties().set("BootRepackage", - RepackageTask.class); - } - - /** - * Register task input/outputs when classifiers are used. - */ - private static class RegisterInputsOutputsAction implements Action { - - private final RepackageTask task; - - private final Project project; - - RegisterInputsOutputsAction(RepackageTask task) { - this.task = task; - this.project = task.getProject(); - } - - @Override - public void execute(Jar jarTask) { - if ("".equals(jarTask.getClassifier())) { - String classifier = this.task.getClassifier(); - if (classifier == null) { - SpringBootPluginExtension extension = this.project.getExtensions() - .getByType(SpringBootPluginExtension.class); - classifier = extension.getClassifier(); - this.task.setClassifier(classifier); - } - if (classifier != null) { - setupInputOutputs(jarTask, classifier); - } - } - } - - private void setupInputOutputs(Jar jarTask, String classifier) { - Logger logger = this.project.getLogger(); - logger.debug("Using classifier: " + classifier + " for task " - + this.task.getName()); - File inputFile = jarTask.getArchivePath(); - String outputName = inputFile.getName(); - outputName = StringUtils.stripFilenameExtension(outputName) + "-" + classifier - + "." + StringUtils.getFilenameExtension(outputName); - File outputFile = new File(inputFile.getParentFile(), outputName); - this.task.getInputs().file(jarTask); - addLibraryDependencies(this.task); - this.task.getOutputs().file(outputFile); - this.task.setOutputFile(outputFile); - } - - private void addLibraryDependencies(final RepackageTask task) { - try { - task.getLibraries().doWithLibraries(new LibraryCallback() { - @Override - public void library(Library library) throws IOException { - task.getInputs().file(library.getFile()); - } - }); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - } - -} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackageTask.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackageTask.java deleted file mode 100644 index 155adebca2..0000000000 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackageTask.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright 2012-2017 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.gradle.repackage; - -import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.gradle.api.Action; -import org.gradle.api.DefaultTask; -import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.plugins.ExtraPropertiesExtension; -import org.gradle.api.tasks.TaskAction; -import org.gradle.api.tasks.bundling.Jar; - -import org.springframework.boot.gradle.SpringBootPluginExtension; -import org.springframework.boot.loader.tools.DefaultLaunchScript; -import org.springframework.boot.loader.tools.LaunchScript; -import org.springframework.boot.loader.tools.Layout; -import org.springframework.boot.loader.tools.Repackager; -import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningListener; -import org.springframework.util.FileCopyUtils; - -/** - * Repackage task. - * - * @author Phillip Webb - * @author Janne Valkealahti - * @author Andy Wilkinson - */ -public class RepackageTask extends DefaultTask { - - private String customConfiguration; - - private Object withJarTask; - - private String mainClass; - - private String classifier; - - private File outputFile; - - private Boolean excludeDevtools; - - private Boolean executable; - - private File embeddedLaunchScript; - - private Map embeddedLaunchScriptProperties; - - public void setCustomConfiguration(String customConfiguration) { - this.customConfiguration = customConfiguration; - } - - public Object getWithJarTask() { - return this.withJarTask; - } - - public void setWithJarTask(Object withJarTask) { - this.withJarTask = withJarTask; - } - - public void setMainClass(String mainClass) { - this.mainClass = mainClass; - } - - public String getMainClass() { - return this.mainClass; - } - - public String getClassifier() { - return this.classifier; - } - - public void setClassifier(String classifier) { - this.classifier = classifier; - } - - void setOutputFile(File file) { - this.outputFile = file; - } - - public Boolean getExcludeDevtools() { - return this.excludeDevtools; - } - - public void setExcludeDevtools(Boolean excludeDevtools) { - this.excludeDevtools = excludeDevtools; - } - - public Boolean getExecutable() { - return this.executable; - } - - public void setExecutable(Boolean executable) { - this.executable = executable; - } - - public File getEmbeddedLaunchScript() { - return this.embeddedLaunchScript; - } - - public void setEmbeddedLaunchScript(File embeddedLaunchScript) { - this.embeddedLaunchScript = embeddedLaunchScript; - } - - public Map getEmbeddedLaunchScriptProperties() { - return this.embeddedLaunchScriptProperties; - } - - public void setEmbeddedLaunchScriptProperties( - Map embeddedLaunchScriptProperties) { - this.embeddedLaunchScriptProperties = embeddedLaunchScriptProperties; - } - - @TaskAction - public void repackage() { - Project project = getProject(); - SpringBootPluginExtension extension = project.getExtensions() - .getByType(SpringBootPluginExtension.class); - ProjectLibraries libraries = getLibraries(); - project.getTasks().withType(Jar.class, new RepackageAction(extension, libraries)); - } - - public ProjectLibraries getLibraries() { - Project project = getProject(); - SpringBootPluginExtension extension = project.getExtensions() - .getByType(SpringBootPluginExtension.class); - ProjectLibraries libraries = new ProjectLibraries(project, extension, - this.excludeDevtools == null ? extension.isExcludeDevtools() - : this.excludeDevtools); - if (extension.getProvidedConfiguration() != null) { - libraries.setProvidedConfigurationName(extension.getProvidedConfiguration()); - } - if (this.customConfiguration != null) { - libraries.setCustomConfigurationName(this.customConfiguration); - } - else if (extension.getCustomConfiguration() != null) { - libraries.setCustomConfigurationName(extension.getCustomConfiguration()); - } - return libraries; - } - - /** - * Action to repackage JARs. - */ - private class RepackageAction implements Action { - - private final SpringBootPluginExtension extension; - - private final ProjectLibraries libraries; - - RepackageAction(SpringBootPluginExtension extension, ProjectLibraries libraries) { - this.extension = extension; - this.libraries = libraries; - } - - @Override - public void execute(Jar jarTask) { - if (!RepackageTask.this.isEnabled()) { - getLogger().info("Repackage disabled"); - return; - } - Object withJarTask = RepackageTask.this.withJarTask; - if (!isTaskMatch(jarTask, withJarTask)) { - getLogger().info( - "Jar task not repackaged (didn't match withJarTask): " + jarTask); - return; - } - File file = jarTask.getArchivePath(); - if (file.exists()) { - repackage(file); - } - } - - private boolean isTaskMatch(Jar task, Object withJarTask) { - if (withJarTask == null) { - if ("".equals(task.getClassifier())) { - Set tasksWithCustomRepackaging = new HashSet<>(); - for (RepackageTask repackageTask : RepackageTask.this.getProject() - .getTasks().withType(RepackageTask.class)) { - if (repackageTask.getWithJarTask() != null) { - tasksWithCustomRepackaging - .add(repackageTask.getWithJarTask()); - } - } - return !tasksWithCustomRepackaging.contains(task); - } - return false; - } - return task.equals(withJarTask) || task.getName().equals(withJarTask); - } - - private void repackage(File file) { - File outputFile = RepackageTask.this.outputFile; - if (outputFile != null && !file.equals(outputFile)) { - copy(file, outputFile); - file = outputFile; - } - Repackager repackager = new Repackager(file, - this.extension.getLayoutFactory()); - repackager.addMainClassTimeoutWarningListener( - new LoggingMainClassTimeoutWarningListener()); - setMainClass(repackager); - Layout layout = this.extension.convertLayout(); - if (layout != null) { - repackager.setLayout(layout); - } - repackager.setBackupSource(this.extension.isBackupSource()); - try { - LaunchScript launchScript = getLaunchScript(); - repackager.repackage(file, this.libraries, launchScript); - } - catch (IOException ex) { - throw new IllegalStateException(ex.getMessage(), ex); - } - } - - private void copy(File source, File dest) { - try { - FileCopyUtils.copy(source, dest); - } - catch (IOException ex) { - throw new IllegalStateException(ex.getMessage(), ex); - } - } - - private void setMainClass(Repackager repackager) { - String mainClassName = getMainClassNameProperty(); - if (RepackageTask.this.mainClass != null) { - mainClassName = RepackageTask.this.mainClass; - } - else if (this.extension.getMainClass() != null) { - mainClassName = this.extension.getMainClass(); - } - else { - Task runTask = getProject().getTasks().findByName("run"); - if (runTask != null && runTask.hasProperty("main")) { - mainClassName = (String) getProject().getTasks().getByName("run") - .property("main"); - } - } - if (mainClassName != null) { - getLogger().info("Setting mainClass: " + mainClassName); - repackager.setMainClass(mainClassName); - } - else { - getLogger().info("No mainClass configured"); - } - } - - private String getMainClassNameProperty() { - if (getProject().hasProperty("mainClassName")) { - return (String) getProject().property("mainClassName"); - } - ExtraPropertiesExtension extraProperties = (ExtraPropertiesExtension) getProject() - .getExtensions().getByName("ext"); - if (extraProperties.has("mainClassName")) { - return (String) extraProperties.get("mainClassName"); - } - return null; - } - - private LaunchScript getLaunchScript() throws IOException { - if (isExecutable() || getEmbeddedLaunchScript() != null) { - return new DefaultLaunchScript(getEmbeddedLaunchScript(), - getEmbeddedLaunchScriptProperties()); - } - return null; - } - - private boolean isExecutable() { - return RepackageTask.this.executable != null ? RepackageTask.this.executable - : this.extension.isExecutable(); - } - - private File getEmbeddedLaunchScript() { - return RepackageTask.this.embeddedLaunchScript != null - ? RepackageTask.this.embeddedLaunchScript - : this.extension.getEmbeddedLaunchScript(); - } - - private Map getEmbeddedLaunchScriptProperties() { - return RepackageTask.this.embeddedLaunchScriptProperties != null - ? RepackageTask.this.embeddedLaunchScriptProperties - : this.extension.getEmbeddedLaunchScriptProperties(); - } - - } - - /** - * {@link Repackager} that also logs when searching takes too long. - */ - private class LoggingMainClassTimeoutWarningListener - implements MainClassTimeoutWarningListener { - - @Override - public void handleTimeoutWarning(long duration, String mainMethod) { - getLogger().warn("Searching for the main-class is taking " - + "some time, consider using setting " + "'springBoot.mainClass'"); - } - - } - -} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveIntegrationTests.java new file mode 100644 index 0000000000..9ab3e301c9 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveIntegrationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.IOException; + +import org.gradle.testkit.runner.InvalidRunnerConfigurationException; +import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.boot.gradle.testkit.GradleBuild; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link BootJar}. + * + * @author Andy Wilkinson + */ +public abstract class AbstractBootArchiveIntegrationTests { + + @Rule + public final GradleBuild gradleBuild = new GradleBuild(); + + private final String taskName; + + protected AbstractBootArchiveIntegrationTests(String taskName) { + this.taskName = taskName; + } + + @Test + public void basicBuild() throws InvalidRunnerConfigurationException, + UnexpectedBuildFailure, IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @Test + public void upToDateWhenBuiltTwice() throws InvalidRunnerConfigurationException, + UnexpectedBuildFailure, IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); + } + + @Test + public void upToDateWhenBuiltTwiceWithLaunchScriptIncluded() + throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, + IOException { + assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName) + .task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.UP_TO_DATE); + } + + @Test + public void notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded() { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @Test + public void notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded() { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @Test + public void notUpToDateWhenLaunchScriptPropertyChanges() { + assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", + "-PlaunchScriptProperty=foo", this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", + "-PlaunchScriptProperty=bar", this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java new file mode 100644 index 0000000000..000691d54d --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java @@ -0,0 +1,234 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.jar.JarFile; + +import org.gradle.api.Project; +import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.springframework.boot.loader.tools.DefaultLaunchScript; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Abstract base class for testing {@link BootArchive} implementations. + * + * @param the type of the concrete BootArchive implementation + * @author Andy Wilkinson + */ +public abstract class AbstractBootArchiveTests { + + @Rule + public final TemporaryFolder temp = new TemporaryFolder(); + + private final Class taskClass; + + private final String launcherClass; + + private final String libPath; + + private final String classesPath; + + private T task; + + protected AbstractBootArchiveTests(Class taskClass, String launcherClass, + String libPath, String classesPath) { + this.taskClass = taskClass; + this.launcherClass = launcherClass; + this.libPath = libPath; + this.classesPath = classesPath; + } + + @Before + public void createTask() { + try { + Project project = ProjectBuilder.builder() + .withProjectDir(this.temp.newFolder()).build(); + this.task = configure( + project.getTasks().create("testArchive", this.taskClass)); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @Test + public void basicArchiveCreation() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.execute(); + assertThat(this.task.getArchivePath().exists()); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class")) + .isEqualTo(this.launcherClass); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class")) + .isEqualTo("com.example.Main"); + } + } + + @Test + public void classpathJarsArePackagedBeneathLibPath() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.classpath(this.temp.newFile("one.jar"), this.temp.newFile("two.jar")); + this.task.execute(); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(jarFile.getEntry(this.libPath + "/one.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "/two.jar")).isNotNull(); + } + } + + @Test + public void classpathFoldersArePackagedBeneathClassesPath() throws IOException { + this.task.setMainClass("com.example.Main"); + File classpathFolder = this.temp.newFolder(); + File applicationClass = new File(classpathFolder, + "com/example/Application.class"); + applicationClass.getParentFile().mkdirs(); + applicationClass.createNewFile(); + this.task.classpath(classpathFolder); + this.task.execute(); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat( + jarFile.getEntry(this.classesPath + "/com/example/Application.class")) + .isNotNull(); + } + } + + @Test + public void loaderIsWrittenToTheRootOfTheJar() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.execute(); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(jarFile.getEntry( + "org/springframework/boot/loader/LaunchedURLClassLoader.class")) + .isNotNull(); + } + } + + @Test + public void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.classpath(this.temp.newFile("one.jar"), this.temp.newFile("two.jar")); + this.task.requiresUnpack("**/one.jar"); + this.task.execute(); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(jarFile.getEntry(this.libPath + "/one.jar").getComment()) + .startsWith("UNPACK:"); + assertThat(jarFile.getEntry(this.libPath + "/two.jar").getComment()).isNull(); + } + } + + @Test + public void unpackCommentIsAddedToEntryIdentifiedByASpec() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.classpath(this.temp.newFile("one.jar"), this.temp.newFile("two.jar")); + this.task.requiresUnpack((element) -> element.getName().endsWith("two.jar")); + this.task.execute(); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(jarFile.getEntry(this.libPath + "/two.jar").getComment()) + .startsWith("UNPACK:"); + assertThat(jarFile.getEntry(this.libPath + "/one.jar").getComment()).isNull(); + } + } + + @Test + public void launchScriptCanBePrepended() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.getLaunchScript().setIncluded(true); + this.task.execute(); + assertThat(Files.readAllBytes(this.task.getArchivePath().toPath())) + .startsWith(new DefaultLaunchScript(null, null).toByteArray()); + } + + @Test + public void customLaunchScriptCanBePrepended() throws IOException { + this.task.setMainClass("com.example.Main"); + LaunchScriptConfiguration launchScript = this.task.getLaunchScript(); + launchScript.setIncluded(true); + File customScript = this.temp.newFile("custom.script"); + Files.write(customScript.toPath(), Arrays.asList("custom script"), + StandardOpenOption.CREATE); + launchScript.setScript(customScript); + this.task.execute(); + assertThat(Files.readAllBytes(this.task.getArchivePath().toPath())) + .startsWith("custom script".getBytes()); + } + + @Test + public void launchScriptPropertiesAreReplaced() throws IOException { + this.task.setMainClass("com.example.Main"); + LaunchScriptConfiguration launchScript = this.task.getLaunchScript(); + launchScript.setIncluded(true); + launchScript.getProperties().put("initInfoProvides", "test property value"); + this.task.execute(); + assertThat(Files.readAllBytes(this.task.getArchivePath().toPath())) + .containsSequence("test property value".getBytes()); + } + + @Test + public void customMainClassInTheManifestIsHonored() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.getManifest().getAttributes().put("Main-Class", + "com.example.CustomLauncher"); + this.task.execute(); + assertThat(this.task.getArchivePath().exists()); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class")) + .isEqualTo("com.example.CustomLauncher"); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class")) + .isEqualTo("com.example.Main"); + } + } + + @Test + public void customStartClassInTheManifestIsHonored() throws IOException { + this.task.setMainClass("com.example.Main"); + this.task.getManifest().getAttributes().put("Start-Class", + "com.example.CustomMain"); + this.task.execute(); + assertThat(this.task.getArchivePath().exists()); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class")) + .isEqualTo(this.launcherClass); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class")) + .isEqualTo("com.example.CustomMain"); + } + } + + private T configure(T task) throws IOException { + AbstractArchiveTask archiveTask = task; + archiveTask.setBaseName("test"); + archiveTask.setDestinationDir(this.temp.newFolder()); + return task; + } + + protected T getTask() { + return this.task; + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.java new file mode 100644 index 0000000000..6a70d391a4 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +/** + * Integration tests for {@link BootJar}. + * + * @author Andy Wilkinson + */ +public class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { + + public BootJarIntegrationTests() { + super("bootJar"); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarTests.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarTests.java new file mode 100644 index 0000000000..f54198e94e --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +/** + * Tests for {@link BootJar}. + * + * @author Andy Wilkinson + */ +public class BootJarTests extends AbstractBootArchiveTests { + + public BootJarTests() { + super(BootJar.class, "org.springframework.boot.loader.JarLauncher", + "BOOT-INF/lib", "BOOT-INF/classes"); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.java new file mode 100644 index 0000000000..c6fbd9c73f --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +/** + * Integration tests for {@link BootJar}. + * + * @author Andy Wilkinson + */ +public class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests { + + public BootWarIntegrationTests() { + super("bootWar"); + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarTests.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarTests.java new file mode 100644 index 0000000000..383d6350d1 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2017 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.gradle.bundling; + +import java.io.IOException; +import java.util.jar.JarFile; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BootWar}. + * + * @author Andy Wilkinson + */ +public class BootWarTests extends AbstractBootArchiveTests { + + public BootWarTests() { + super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib", + "WEB-INF/classes"); + } + + @Test + public void providedClasspathJarsArePackagedInWebInfLibProvided() throws IOException { + getTask().setMainClass("com.example.Main"); + getTask().providedClasspath(this.temp.newFile("one.jar"), + this.temp.newFile("two.jar")); + getTask().execute(); + try (JarFile jarFile = new JarFile(getTask().getArchivePath())) { + assertThat(jarFile.getEntry("WEB-INF/lib-provided/one.jar")).isNotNull(); + assertThat(jarFile.getEntry("WEB-INF/lib-provided/two.jar")).isNotNull(); + } + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java new file mode 100644 index 0000000000..4bde3bea62 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2017 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.gradle.testkit; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.spring.gradle.dependencymanagement.DependencyManagementPlugin; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import org.springframework.boot.loader.tools.LaunchScript; + +/** + * A {@link TestRule} for running a Gradle build using {@link GradleRunner}. + * + * @author Andy Wilkinson + */ +public class GradleBuild implements TestRule { + + private final TemporaryFolder temp = new TemporaryFolder(); + + private File projectDir; + + private String script; + + @Override + public Statement apply(Statement base, Description description) { + String name = description.getTestClass().getSimpleName() + ".gradle"; + URL scriptUrl = description.getTestClass().getResource(name); + if (scriptUrl != null) { + script(scriptUrl.getFile()); + } + return this.temp.apply(new Statement() { + + @Override + public void evaluate() throws Throwable { + before(); + try { + base.evaluate(); + } + finally { + after(); + } + } + + }, description); + } + + private void before() throws IOException { + this.projectDir = this.temp.newFolder(); + } + + private void after() { + GradleBuild.this.script = null; + } + + private String pluginClasspath() { + return new File("build/classes/main").getAbsolutePath() + "," + + new File("build/resources/main").getAbsolutePath() + "," + + LaunchScript.class.getProtectionDomain().getCodeSource().getLocation() + .getPath() + + "," + DependencyManagementPlugin.class.getProtectionDomain() + .getCodeSource().getLocation().getPath(); + } + + public GradleBuild script(String script) { + this.script = script; + return this; + } + + public BuildResult build(String... arguments) { + try { + Files.copy(new File(this.script).toPath(), + new File(this.projectDir, "build.gradle").toPath(), + StandardCopyOption.REPLACE_EXISTING); + GradleRunner gradleRunner = GradleRunner.create() + .withProjectDir(this.projectDir).forwardOutput(); + List allArguments = new ArrayList(); + allArguments.add("-PpluginClasspath=" + pluginClasspath()); + allArguments.addAll(Arrays.asList(arguments)); + return gradleRunner.withArguments(allArguments).build(); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.gradle b/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.gradle new file mode 100644 index 0000000000..b54d7a8ecb --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.gradle @@ -0,0 +1,16 @@ +buildscript { + dependencies { + classpath files(pluginClasspath.split(',')) + } +} + +apply plugin: 'java' +apply plugin: 'org.springframework.boot' + +bootJar { + mainClass = 'com.example.Application' + launchScript { + included = project.hasProperty('includeLaunchScript') ? includeLaunchScript : false + properties 'prop' : project.hasProperty('launchScriptProperty') ? launchScriptProperty : 'default' + } +} diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.gradle b/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.gradle new file mode 100644 index 0000000000..3461c7fe00 --- /dev/null +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.gradle @@ -0,0 +1,16 @@ +buildscript { + dependencies { + classpath files(pluginClasspath.split(',')) + } +} + +apply plugin: 'war' +apply plugin: 'org.springframework.boot' + +bootWar { + mainClass = 'com.example.Application' + launchScript { + included = project.hasProperty('includeLaunchScript') ? includeLaunchScript : false + properties 'prop' : project.hasProperty('launchScriptProperty') ? launchScriptProperty : 'default' + } +}