org.springframework.boot.gradle.tasks.bundling.BootZipCopyAction.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.boot.gradle.tasks.bundling.BootZipCopyAction.java

Source

/*
 * Copyright 2012-2018 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.Calendar;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.zip.CRC32;
import java.util.zip.ZipInputStream;

import org.apache.commons.compress.archivers.zip.UnixStat;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
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.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 {

    static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0)
            .getTimeInMillis();

    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;

    private final String encoding;

    BootZipCopyAction(File output, boolean preserveFileTimestamps, boolean includeDefaultLoader,
            Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
            LaunchScriptConfiguration launchScript, Function<FileCopyDetails, ZipCompression> compressionResolver,
            String encoding) {
        this.output = output;
        this.preserveFileTimestamps = preserveFileTimestamps;
        this.includeDefaultLoader = includeDefaultLoader;
        this.requiresUnpack = requiresUnpack;
        this.exclusions = exclusions;
        this.launchScript = launchScript;
        this.compressionResolver = compressionResolver;
        this.encoding = encoding;
    }

    @Override
    public WorkResult execute(CopyActionProcessingStream stream) {
        ZipArchiveOutputStream zipStream;
        Spec<FileTreeElement> loaderEntries;
        try {
            FileOutputStream fileStream = new FileOutputStream(this.output);
            writeLaunchScriptIfNecessary(fileStream);
            zipStream = new ZipArchiveOutputStream(fileStream);
            if (this.encoding != null) {
                zipStream.setEncoding(this.encoding);
            }
            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(ZipArchiveOutputStream out) {
        if (!this.includeDefaultLoader) {
            return Specs.satisfyNone();
        }
        return writeLoaderClasses(out);
    }

    private Spec<FileTreeElement> writeLoaderClasses(ZipArchiveOutputStream out) {
        try (ZipInputStream in = new ZipInputStream(
                getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
            Set<String> entries = new HashSet<>();
            java.util.zip.ZipEntry entry;
            while ((entry = in.getNextEntry()) != null) {
                if (entry.isDirectory() && !entry.getName().startsWith("META-INF/")) {
                    writeDirectory(new ZipArchiveEntry(entry), out);
                    entries.add(entry.getName());
                } else if (entry.getName().endsWith(".class")) {
                    writeClass(new ZipArchiveEntry(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(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException {
        prepareEntry(entry, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
        out.putArchiveEntry(entry);
        out.closeArchiveEntry();
    }

    private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out)
            throws IOException {
        prepareEntry(entry, UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
        out.putArchiveEntry(entry);
        byte[] buffer = new byte[4096];
        int read;
        while ((read = in.read(buffer)) > 0) {
            out.write(buffer, 0, read);
        }
        out.closeArchiveEntry();
    }

    private void prepareEntry(ZipArchiveEntry entry, int unixMode) {
        if (!this.preserveFileTimestamps) {
            entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES);
        }
        entry.setUnixMode(unixMode);
    }

    private void writeLaunchScriptIfNecessary(FileOutputStream fileStream) {
        try {
            if (this.launchScript != null) {
                fileStream.write(
                        new DefaultLaunchScript(this.launchScript.getScript(), this.launchScript.getProperties())
                                .toByteArray());
                this.output.setExecutable(true);
            }
        } catch (IOException ex) {
            throw new GradleException("Failed to write launch script to " + this.output, ex);
        }
    }

    private static final class ZipStreamAction implements CopyActionProcessingStreamAction {

        private final ZipArchiveOutputStream 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(ZipArchiveOutputStream 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 {
            ZipArchiveEntry archiveEntry = new ZipArchiveEntry(details.getRelativePath().getPathString() + '/');
            archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
            archiveEntry.setTime(getTime(details));
            this.zipStream.putArchiveEntry(archiveEntry);
            this.zipStream.closeArchiveEntry();
        }

        private void createFile(FileCopyDetailsInternal details) throws IOException {
            String relativePath = details.getRelativePath().getPathString();
            ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath);
            archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
            archiveEntry.setTime(getTime(details));
            ZipCompression compression = this.compressionType.apply(details);
            if (compression == ZipCompression.STORED) {
                prepareStoredEntry(details, archiveEntry);
            }
            this.zipStream.putArchiveEntry(archiveEntry);
            details.copyTo(this.zipStream);
            this.zipStream.closeArchiveEntry();
        }

        private void prepareStoredEntry(FileCopyDetailsInternal details, ZipArchiveEntry archiveEntry)
                throws IOException {
            archiveEntry.setMethod(java.util.zip.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() : 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();
        }

    }

}