|  | /* | 
|  | * Copyright 2008 Google Inc. | 
|  | * | 
|  | * 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.gwt.ant.taskdefs; | 
|  |  | 
|  | import org.apache.tools.ant.BuildException; | 
|  | import org.apache.tools.ant.Project; | 
|  | import org.apache.tools.ant.taskdefs.Jar; | 
|  | import org.apache.tools.zip.ZipExtraField; | 
|  | import org.apache.tools.zip.ZipOutputStream; | 
|  |  | 
|  | import java.io.File; | 
|  | import java.io.FileInputStream; | 
|  | import java.io.FileOutputStream; | 
|  | import java.io.IOException; | 
|  | import java.io.InputStream; | 
|  | import java.io.OutputStream; | 
|  | import java.util.Map; | 
|  | import java.util.Random; | 
|  | import java.util.TreeMap; | 
|  |  | 
|  | /** | 
|  | * A variation on Jar which handles duplicate entries by only archiving the most | 
|  | * recent of any given path. This is done by keeping a map of paths (as shown in | 
|  | * the jar file) against {@link EntryInfo} objects identifying the input source | 
|  | * and its timestamp. Most of the actual archiving is deferred until archive | 
|  | * finalization, when we've decided on the actual de-duplicated set of entries. | 
|  | */ | 
|  | public class LatestTimeJar extends Jar { | 
|  |  | 
|  | /** | 
|  | * Metadata about pending entries in the jar, for replacement if newer entries | 
|  | * are found. Subclasses of EntryInfo are held in the | 
|  | */ | 
|  | protected abstract class EntryInfo { | 
|  | protected long timestamp; | 
|  | protected int mode; | 
|  |  | 
|  | public EntryInfo(long lastModified, int mode) { | 
|  | this.timestamp = lastModified; | 
|  | this.mode = mode; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Called to actually add the entry to a given zip stream. | 
|  | * | 
|  | * @param out | 
|  | * @param path | 
|  | * @throws IOException | 
|  | */ | 
|  | public abstract void addToZip(ZipOutputStream out, String path) | 
|  | throws IOException; | 
|  |  | 
|  | public long getLastModified() { | 
|  | return timestamp; | 
|  | } | 
|  |  | 
|  | public int getMode() { | 
|  | return mode; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Metadata about a directory entry. | 
|  | */ | 
|  | protected class DirEntryInfo extends EntryInfo { | 
|  | protected File dir; | 
|  | protected ZipExtraField extra[]; | 
|  |  | 
|  | public DirEntryInfo(File dir, long touchTime, int mode, | 
|  | ZipExtraField extra[]) { | 
|  | super(touchTime, mode); | 
|  | this.dir = dir; | 
|  | this.extra = extra; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void addToZip(ZipOutputStream out, String path) throws IOException { | 
|  | doZipDir(dir, out, path, mode, extra); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Metadata about a file entry. | 
|  | */ | 
|  | protected class FileEntryInfo extends EntryInfo { | 
|  | private File tmpFile; | 
|  | private File archive; | 
|  |  | 
|  | public FileEntryInfo(InputStream in, long lastModified, File fromArchive, | 
|  | int mode) throws IOException { | 
|  | super(lastModified, mode); | 
|  | tmpFile = createTempFile("gwtjar", ""); | 
|  | tmpFile.deleteOnExit(); | 
|  | OutputStream fos = new FileOutputStream(tmpFile); | 
|  | int readLen = in.read(buffer); | 
|  | while (readLen > 0) { | 
|  | fos.write(buffer, 0, readLen); | 
|  | readLen = in.read(buffer); | 
|  | } | 
|  | fos.close(); | 
|  | archive = fromArchive; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void addToZip(ZipOutputStream out, String path) throws IOException { | 
|  | FileInputStream inStream = new FileInputStream(tmpFile); | 
|  | try { | 
|  | doZipFile(inStream, out, path, timestamp, archive, mode); | 
|  | tmpFile.delete(); | 
|  | } finally { | 
|  | inStream.close(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Used to generate temporary file names. | 
|  | */ | 
|  | private static long counter = -1; | 
|  |  | 
|  | /** | 
|  | * Creates a temporary file. | 
|  | * | 
|  | * @param prefix the file prefix | 
|  | * @param suffix the file suffix | 
|  | * @return the new file | 
|  | * @throws IOException if the file cannot be created | 
|  | */ | 
|  | private static File createTempFile(String prefix, String suffix) | 
|  | throws IOException { | 
|  | if (suffix == null) { | 
|  | suffix = ".tmp"; | 
|  | } | 
|  |  | 
|  | // Get the temp file directory. | 
|  | File tmpDir = new File(System.getProperty("java.io.tmpdir")); | 
|  | tmpDir.mkdirs(); | 
|  |  | 
|  | // Generate a random name. | 
|  | if (counter == -1) { | 
|  | counter = new Random().nextLong(); | 
|  | } | 
|  | boolean created = false; | 
|  | File tmpFile; | 
|  | do { | 
|  | counter++; | 
|  | tmpFile = new File(tmpDir, prefix + Long.toString(counter) + suffix); | 
|  | if (!tmpFile.exists()) { | 
|  | created = tmpFile.createNewFile(); | 
|  | if (!created) { | 
|  | // If we fail the create the temp file, it must have been created by | 
|  | // another thread between lines 161 and 162.  We re-seed to avoid | 
|  | // further race conditions. | 
|  | counter = new Random().nextLong(); | 
|  | } | 
|  | } | 
|  | } while (!created); | 
|  |  | 
|  | // Create the file. | 
|  | tmpFile.createNewFile(); | 
|  | return tmpFile; | 
|  | } | 
|  |  | 
|  | private byte buffer[] = new byte[16 * 1024]; | 
|  | private Map<String, EntryInfo> paths = new TreeMap<String, EntryInfo>(); | 
|  |  | 
|  | @Override | 
|  | protected void finalizeZipOutputStream(ZipOutputStream out) | 
|  | throws IOException, BuildException { | 
|  | for (String path : paths.keySet()) { | 
|  | paths.get(path).addToZip(out, path); | 
|  | } | 
|  | super.finalizeZipOutputStream(out); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void zipDir(File dir, | 
|  | @SuppressWarnings("unused") ZipOutputStream out, String path, int mode, | 
|  | ZipExtraField[] extra) throws IOException { | 
|  | long thisTouchTime = (dir == null ? 0L : dir.lastModified()); | 
|  | String dirName = (dir == null ? "<null>" : dir.getAbsolutePath()); | 
|  | if (shouldReplace(path, thisTouchTime)) { | 
|  | if (paths.get(path) != null) { | 
|  | log("Obsoleting older " + path + " with " + dirName, | 
|  | Project.MSG_VERBOSE); | 
|  | } | 
|  | paths.put(path, new DirEntryInfo(dir, thisTouchTime, mode, extra)); | 
|  | } else { | 
|  | log("Newer " + path + " already added, skipping " + dirName, | 
|  | Project.MSG_VERBOSE); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void zipFile(InputStream in, | 
|  | @SuppressWarnings("unused") ZipOutputStream out, String path, | 
|  | long lastModified, File fromArchive, int mode) throws IOException { | 
|  |  | 
|  | String desc = (fromArchive == null ? "file" : "file from " | 
|  | + fromArchive.getAbsolutePath()); | 
|  |  | 
|  | if (shouldReplace(path, lastModified)) { | 
|  | if (paths.get(path) != null) { | 
|  | log("Obsoleting older " + path + " with " + desc, Project.MSG_VERBOSE); | 
|  | } | 
|  | paths.put(path, new FileEntryInfo(in, lastModified, fromArchive, mode)); | 
|  | } else { | 
|  | log("Newer " + path + " already added, skipping " + desc, | 
|  | Project.MSG_VERBOSE); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void doZipDir(File dir, ZipOutputStream out, String entryName, | 
|  | int mode, ZipExtraField[] extra) throws IOException { | 
|  | super.zipDir(dir, out, entryName, mode, extra); | 
|  | } | 
|  |  | 
|  | private void doZipFile(InputStream inStream, ZipOutputStream out, | 
|  | String entryName, long timestamp, File archive, int mode) | 
|  | throws IOException { | 
|  | super.zipFile(inStream, out, entryName, timestamp, archive, mode); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Checks whether an entry should be replaced, by touch dates and duplicates | 
|  | * setting. | 
|  | * | 
|  | * @param path the path of an entry being considered | 
|  | * @param touchTime the lastModified of the candiate replacement | 
|  | * @return true if the file should be replaced | 
|  | */ | 
|  | private boolean shouldReplace(String path, long touchTime) { | 
|  | EntryInfo oldInfo = paths.get(path); | 
|  | // adding from jars, we get directories with 0L time; missing should be | 
|  | // earlier than that, -1L. | 
|  | long existingTouchTime = ((oldInfo != null) ? oldInfo.getLastModified() | 
|  | : -1L); | 
|  | return (existingTouchTime < touchTime || (existingTouchTime == touchTime && this.duplicate.equals("add"))); | 
|  | } | 
|  | } |