diff options
Diffstat (limited to 'src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JarHelper.java')
-rw-r--r-- | src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JarHelper.java | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JarHelper.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JarHelper.java new file mode 100644 index 0000000000..0832082707 --- /dev/null +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JarHelper.java @@ -0,0 +1,201 @@ +// 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(); + } + } + } +} |