com.google.devtools.build.buildjar.JarHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.buildjar.JarHelper.java

Source

// Copyright 2014 Google Inc. All rights reserved.
//
// 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 com.google.devtools.build.buildjar;

import com.google.common.hash.Hashing;
import com.google.common.io.Files;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
 * A simple helper class for creating Jar files. All Jar entries are sorted alphabetically. Allows
 * normalization of Jar entries by setting the timestamp of non-.class files to the DOS epoch.
 * Timestamps of .class files are set to the DOS epoch + 2 seconds (The zip timestamp granularity)
 * Adjusting the timestamp for .class files is neccessary since otherwise javac will recompile java
 * files if both the java file and its .class file are present.
 */
public class JarHelper {

    public static final String MANIFEST_DIR = "META-INF/";
    public static final String MANIFEST_NAME = JarFile.MANIFEST_NAME;
    public static final String SERVICES_DIR = "META-INF/services/";

    public static final long DOS_EPOCH_IN_JAVA_TIME = 315561600000L;

    // ZIP timestamps have a resolution of 2 seconds.
    // see http://www.info-zip.org/FAQ.html#limits
    public static final long MINIMUM_TIMESTAMP_INCREMENT = 2000L;

    // The name of the Jar file we want to create
    protected final String jarFile;

    // The properties to describe how to create the Jar
    protected boolean normalize;
    protected int storageMethod = JarEntry.DEFLATED;
    protected boolean verbose = false;

    // The state needed to create the Jar
    protected final Set<String> names = new HashSet<>();
    protected JarOutputStream out;

    public JarHelper(String filename) {
        jarFile = filename;
    }

    /**
     * Enables or disables the Jar entry normalization.
     *
     * @param normalize If true the timestamps of Jar entries will be set to the
     *        DOS epoch.
     */
    public void setNormalize(boolean normalize) {
        this.normalize = normalize;
    }

    /**
     * Enables or disables compression for the Jar file entries.
     *
     * @param compression if true enables compressions for the Jar file entries.
     */
    public void setCompression(boolean compression) {
        storageMethod = compression ? JarEntry.DEFLATED : JarEntry.STORED;
    }

    /**
     * Enables or disables verbose messages.
     *
     * @param verbose if true enables verbose messages.
     */
    public void setVerbose(boolean verbose) {
        this.verbose = verbose;
    }

    /**
     * Returns the normalized timestamp for a jar entry based on its name.
     * This is necessary since javac will, when loading a class X, prefer a
     * source file to a class file, if both files have the same timestamp.
     * Therefore, we need to adjust the timestamp for class files to slightly
     * after the normalized time.
     * @param name The name of the file for which we should return the
     *     normalized timestamp.
     * @return the time for a new Jar file entry in milliseconds since the epoch.
     */
    private long normalizedTimestamp(String name) {
        if (name.endsWith(".class")) {
            return DOS_EPOCH_IN_JAVA_TIME + MINIMUM_TIMESTAMP_INCREMENT;
        } else {
            return DOS_EPOCH_IN_JAVA_TIME;
        }
    }

    /**
     * Returns the time for a new Jar file entry in milliseconds since the epoch.
     * Uses {@link JarCreator#DOS_EPOCH_IN_JAVA_TIME} for normalized entries,
     * {@link System#currentTimeMillis()} otherwise.
     *
     * @param filename The name of the file for which we are entering the time
     * @return the time for a new Jar file entry in milliseconds since the epoch.
     */
    protected long newEntryTimeMillis(String filename) {
        return normalize ? normalizedTimestamp(filename) : System.currentTimeMillis();
    }

    /**
     * Writes an entry with specific contents to the jar. Directory entries must
     * include the trailing '/'.
     */
    protected void writeEntry(JarOutputStream out, String name, byte[] content) throws IOException {
        if (names.add(name)) {
            // Create a new entry
            JarEntry entry = new JarEntry(name);
            entry.setTime(newEntryTimeMillis(name));
            int size = content.length;
            entry.setSize(size);
            if (size == 0) {
                entry.setMethod(JarEntry.STORED);
                entry.setCrc(0);
                out.putNextEntry(entry);
            } else {
                entry.setMethod(storageMethod);
                if (storageMethod == JarEntry.STORED) {
                    entry.setCrc(Hashing.crc32().hashBytes(content).padToLong());
                }
                out.putNextEntry(entry);
                out.write(content);
            }
            out.closeEntry();
        }
    }

    /**
     * Writes a standard Java manifest entry into the JarOutputStream. This
     * includes the directory entry for the "META-INF" directory
     *
     * @param content the Manifest content to write to the manifest entry.
     * @throws IOException
     */
    protected void writeManifestEntry(byte[] content) throws IOException {
        writeEntry(out, MANIFEST_DIR, new byte[] {});
        writeEntry(out, MANIFEST_NAME, content);
    }

    /**
     * Copies file or directory entries from the file system into the jar.
     * Directory entries will be detected and their names automatically '/'
     * suffixed.
     */
    protected void copyEntry(String name, File file) throws IOException {
        if (!names.contains(name)) {
            if (!file.exists()) {
                throw new FileNotFoundException(file.getAbsolutePath() + " (No such file or directory)");
            }
            boolean isDirectory = file.isDirectory();
            if (isDirectory && !name.endsWith("/")) {
                name = name + '/'; // always normalize directory names before checking set
            }
            if (names.add(name)) {
                if (verbose) {
                    System.err.println("adding " + file);
                }
                // Create a new entry
                long size = isDirectory ? 0 : file.length();
                JarEntry outEntry = new JarEntry(name);
                long newtime = normalize ? normalizedTimestamp(name) : file.lastModified();
                outEntry.setTime(newtime);
                outEntry.setSize(size);
                if (size == 0L) {
                    outEntry.setMethod(JarEntry.STORED);
                    outEntry.setCrc(0);
                    out.putNextEntry(outEntry);
                } else {
                    outEntry.setMethod(storageMethod);
                    if (storageMethod == JarEntry.STORED) {
                        outEntry.setCrc(Files.hash(file, Hashing.crc32()).padToLong());
                    }
                    out.putNextEntry(outEntry);
                    Files.copy(file, out);
                }
                out.closeEntry();
            }
        }
    }
}