Java tutorial
// 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(); } } } }