Commit 2ce85569 authored by Andy Wilkinson's avatar Andy Wilkinson

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
parent 20fe95b2
......@@ -35,6 +35,7 @@ if (effectivePomFile.file) {
dependencies {
compile localGroovy()
compile gradleApi()
testCompile gradleTestKit()
}
jar {
......
/*
* 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<FileTreeElement> 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<LaunchScriptConfiguration> 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);
}
/*
* 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<String> 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<FileTreeElement> spec) {
this.requiresUnpack.include(spec);
}
}
/*
* 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<CopySpec> classpathFiles(Spec<File> filter) {
return (copySpec) -> {
copySpec.from((Callable<Iterable<File>>) () -> {
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<FileTreeElement> spec) {
this.support.requiresUnpack(spec);
}
@Override
public LaunchScriptConfiguration getLaunchScript() {
return this.support.getLaunchScript();
}
@Override
public void launchScript(Action<LaunchScriptConfiguration> 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);
}
}
/*
* 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<Iterable<File>>) () -> {
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<FileTreeElement> spec) {
this.support.requiresUnpack(spec);
}
@Override
public LaunchScriptConfiguration getLaunchScript() {
return this.support.getLaunchScript();
}
@Override
public void launchScript(Action<LaunchScriptConfiguration> 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);
}
}
/*
* 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<FileTreeElement> requiresUnpack;
private final LaunchScriptConfiguration launchScript;
private final Set<String> storedPathPrefixes;
BootZipCopyAction(File output, Spec<FileTreeElement> requiresUnpack,
LaunchScriptConfiguration launchScript, Set<String> 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<FileTreeElement> requiresUnpack;
private final Set<String> storedPathPrefixes;
private ZipStreamAction(ZipOutputStream zipStream, File output,
Spec<FileTreeElement> requiresUnpack, Set<String> 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();
}
}
}
/*
* 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<FileCollection>) () -> {
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);
}
}
/*
* 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<String, String> properties = new HashMap<String, String>();
private File script;
public boolean isIncluded() {
return this.included;
}
public void setIncluded(boolean included) {
this.included = included;
}
public Map<String, String> getProperties() {
return this.properties;
}
public void properties(Map<String, String> 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;
}
}
}
/*
* 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<String> {
private final Supplier<FileCollection> classpathSupplier;
private String mainClass;
MainClassSupplier(Supplier<FileCollection> 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;
}
}
......@@ -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> {
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());
......
/*
* 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<Project>() {
@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<Jar> {
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);
}
}
}
}
/*
* 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<String, String> 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<String, String> getEmbeddedLaunchScriptProperties() {
return this.embeddedLaunchScriptProperties;
}
public void setEmbeddedLaunchScriptProperties(
Map<String, String> 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<Jar> {
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<Object> 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<String, String> 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'");
}
}
}
/*
* 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);
}
}
/*
* 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 <T> the type of the concrete BootArchive implementation
* @author Andy Wilkinson
*/
public abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
@Rule
public final TemporaryFolder temp = new TemporaryFolder();
private final Class<T> taskClass;
private final String launcherClass;
private final String libPath;
private final String classesPath;
private T task;
protected AbstractBootArchiveTests(Class<T> 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;
}
}
/*
* 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");
}
}
/*
* 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<BootJar> {
public BootJarTests() {
super(BootJar.class, "org.springframework.boot.loader.JarLauncher",
"BOOT-INF/lib", "BOOT-INF/classes");
}
}
/*
* 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");
}
}
/*
* 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<BootWar> {
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();
}
}
}
/*
* 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<String> allArguments = new ArrayList<String>();
allArguments.add("-PpluginClasspath=" + pluginClasspath());
allArguments.addAll(Arrays.asList(arguments));
return gradleRunner.withArguments(allArguments).build();
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
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'
}
}
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'
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment