From 2ce85569761aafa4087e0f4dc3c956ff9fd5219a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 2 Mar 2017 16:46:27 +0000 Subject: [PATCH] Introduce bootJar and bootWar tasks for creating fat jars and wars Previously, the BootRepackage task would take the output of a Jar or War task and repackage it in a similar manner to Spring Boot's Maven plugin. This caused several problems in Gradle including broken up-to-date checks and a lack of configurability. See the issues referenced below for full details. This commit replaces BootRepackage with BootJar and BootWar for building executable jars and wars respectively. BootJar extends Gradle's standard Jar task and BootWar extends Gradle's standard War task. This means that terms of configuration, the creation of executable jars and wars is now as flexible as the creation of standards jars and wars. Closes gh-8167 Closes gh-8099 Closes gh-6846 Closes gh-5861 Closes gh-5393 Closes gh-5259 Closes gh-3931 --- .../spring-boot-gradle-plugin/build.gradle | 1 + .../boot/gradle/bundling/BootArchive.java | 102 ++++++ .../gradle/bundling/BootArchiveSupport.java | 94 +++++ .../boot/gradle/bundling/BootJar.java | 119 +++++++ .../boot/gradle/bundling/BootWar.java | 119 +++++++ .../gradle/bundling/BootZipCopyAction.java | 243 +++++++++++++ .../bundling/BundlingPluginFeatures.java | 67 ++++ .../bundling/LaunchScriptConfiguration.java | 116 ++++++ .../gradle/bundling/MainClassSupplier.java | 72 ++++ .../boot/gradle/plugin/SpringBootPlugin.java | 4 +- .../gradle/repackage/ProjectLibraries.java | 334 ------------------ .../repackage/RepackagePluginFeatures.java | 174 --------- .../boot/gradle/repackage/RepackageTask.java | 322 ----------------- .../AbstractBootArchiveIntegrationTests.java | 100 ++++++ .../bundling/AbstractBootArchiveTests.java | 234 ++++++++++++ .../bundling/BootJarIntegrationTests.java | 30 ++ .../boot/gradle/bundling/BootJarTests.java | 31 ++ .../bundling/BootWarIntegrationTests.java | 30 ++ .../boot/gradle/bundling/BootWarTests.java | 50 +++ .../boot/gradle/testkit/GradleBuild.java | 113 ++++++ .../bundling/BootJarIntegrationTests.gradle | 16 + .../bundling/BootWarIntegrationTests.gradle | 16 + 22 files changed, 1555 insertions(+), 832 deletions(-) create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchive.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootJar.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootWar.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BundlingPluginFeatures.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/LaunchScriptConfiguration.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/MainClassSupplier.java delete mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/ProjectLibraries.java delete mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java delete mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/repackage/RepackageTask.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveIntegrationTests.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootJarTests.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/BootWarTests.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootJarIntegrationTests.gradle create mode 100644 spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/bundling/BootWarIntegrationTests.gradle 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' + } +}