/*
* 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.tasks.bundling;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
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.FileCopyDetails;
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.specs.Specs;
import org.gradle.api.tasks.WorkResult;
import org.gradle.util.GUtil;
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 boolean preserveFileTimestamps;
private final boolean includeDefaultLoader;
private final Spec<FileTreeElement> requiresUnpack;
private final Spec<FileTreeElement> exclusions;
private final LaunchScriptConfiguration launchScript;
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
BootZipCopyAction(File output, boolean preserveFileTimestamps,
boolean includeDefaultLoader, Spec<FileTreeElement> requiresUnpack,
Spec<FileTreeElement> exclusions, LaunchScriptConfiguration launchScript,
Function<FileCopyDetails, ZipCompression> compressionResolver) {
this.output = output;
this.preserveFileTimestamps = preserveFileTimestamps;
this.includeDefaultLoader = includeDefaultLoader;
this.requiresUnpack = requiresUnpack;
this.exclusions = exclusions;
this.launchScript = launchScript;
this.compressionResolver = compressionResolver;
}
@Override
public WorkResult execute(CopyActionProcessingStream stream) {
ZipOutputStream zipStream;
Spec<FileTreeElement> loaderEntries;
try {
FileOutputStream fileStream = new FileOutputStream(this.output);
writeLaunchScriptIfNecessary(fileStream);
zipStream = new ZipOutputStream(fileStream);
loaderEntries = writeLoaderClassesIfNecessary(zipStream);
}
catch (IOException ex) {
throw new GradleException("Failed to create " + this.output, ex);
}
try {
stream.process(new ZipStreamAction(zipStream, this.output,
this.preserveFileTimestamps, this.requiresUnpack,
createExclusionSpec(loaderEntries), this.compressionResolver));
}
finally {
try {
zipStream.close();
}
catch (IOException ex) {
// Continue
}
}
return () -> true;
}
@SuppressWarnings("unchecked")
private Spec<FileTreeElement> createExclusionSpec(
Spec<FileTreeElement> loaderEntries) {
return Specs.union(loaderEntries, this.exclusions);
}
private Spec<FileTreeElement> writeLoaderClassesIfNecessary(ZipOutputStream out) {
if (!this.includeDefaultLoader) {
return Specs.satisfyNone();
}
return writeLoaderClasses(out);
}
private Spec<FileTreeElement> writeLoaderClasses(ZipOutputStream out) {
try (ZipInputStream in = new ZipInputStream(getClass()
.getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
Set<String> entries = new HashSet<String>();
ZipEntry entry;
while ((entry = in.getNextEntry()) != null) {
if (entry.isDirectory() && !entry.getName().startsWith("META-INF/")) {
writeDirectory(entry, out);
entries.add(entry.getName());
}
else if (entry.getName().endsWith(".class")) {
writeClass(entry, in, out);
}
}
return (element) -> {
String path = element.getRelativePath().getPathString();
if (element.isDirectory() && !path.endsWith(("/"))) {
path += "/";
}
return entries.contains(path);
};
}
catch (IOException ex) {
throw new GradleException("Failed to write loader classes", ex);
}
}
private void writeDirectory(ZipEntry entry, ZipOutputStream out) throws IOException {
if (!this.preserveFileTimestamps) {
entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES);
}
out.putNextEntry(entry);
out.closeEntry();
}
private void writeClass(ZipEntry entry, ZipInputStream in, ZipOutputStream out)
throws IOException {
if (!this.preserveFileTimestamps) {
entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES);
}
out.putNextEntry(entry);
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) > 0) {
out.write(buffer, 0, read);
}
out.closeEntry();
}
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 boolean preserveFileTimestamps;
private final Spec<FileTreeElement> requiresUnpack;
private final Spec<FileTreeElement> exclusions;
private final Function<FileCopyDetails, ZipCompression> compressionType;
private ZipStreamAction(ZipOutputStream zipStream, File output,
boolean preserveFileTimestamps, Spec<FileTreeElement> requiresUnpack,
Spec<FileTreeElement> exclusions,
Function<FileCopyDetails, ZipCompression> compressionType) {
this.zipStream = zipStream;
this.output = output;
this.preserveFileTimestamps = preserveFileTimestamps;
this.requiresUnpack = requiresUnpack;
this.exclusions = exclusions;
this.compressionType = compressionType;
}
@Override
public void processFile(FileCopyDetailsInternal details) {
if (this.exclusions.isSatisfiedBy(details)) {
return;
}
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(getTime(details));
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(getTime(details));
ZipCompression compression = this.compressionType.apply(details);
if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, archiveEntry);
}
this.zipStream.putNextEntry(archiveEntry);
details.copyTo(this.zipStream);
this.zipStream.closeEntry();
}
private void prepareStoredEntry(FileCopyDetailsInternal details,
ZipEntry archiveEntry) throws IOException {
archiveEntry.setMethod(ZipEntry.STORED);
archiveEntry.setSize(details.getSize());
archiveEntry.setCompressedSize(details.getSize());
Crc32OutputStream crcStream = new Crc32OutputStream();
details.copyTo(crcStream);
archiveEntry.setCrc(crcStream.getCrc());
if (this.requiresUnpack.isSatisfiedBy(details)) {
archiveEntry
.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
}
}
private long getTime(FileCopyDetails details) {
return this.preserveFileTimestamps ? details.getLastModified()
: GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES;
}
}
/**
* An {@code OutputStream} that provides a CRC-32 of the data that is written to it.
*/
private static final class Crc32OutputStream extends OutputStream {
private final CRC32 crc32 = new CRC32();
@Override
public void write(int b) throws IOException {
this.crc32.update(b);
}
@Override
public void write(byte[] b) throws IOException {
this.crc32.update(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
this.crc32.update(b, off, len);
}
private long getCrc() {
return this.crc32.getValue();
}
}
}