diff options
Diffstat (limited to 'framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Zip.java')
-rw-r--r-- | framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Zip.java | 2274 |
1 files changed, 2274 insertions, 0 deletions
diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Zip.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Zip.java new file mode 100644 index 00000000..ddc9bd49 --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Zip.java @@ -0,0 +1,2274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tools.ant.taskdefs; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.Stack; +import java.util.Vector; +import java.util.zip.CRC32; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.FileScanner; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.types.ArchiveFileSet; +import org.apache.tools.ant.types.EnumeratedAttribute; +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.types.PatternSet; +import org.apache.tools.ant.types.Resource; +import org.apache.tools.ant.types.ResourceCollection; +import org.apache.tools.ant.types.ZipFileSet; +import org.apache.tools.ant.types.ZipScanner; +import org.apache.tools.ant.types.resources.ArchiveResource; +import org.apache.tools.ant.types.resources.FileProvider; +import org.apache.tools.ant.types.resources.FileResource; +import org.apache.tools.ant.types.resources.Union; +import org.apache.tools.ant.types.resources.ZipResource; +import org.apache.tools.ant.types.resources.selectors.ResourceSelector; +import org.apache.tools.ant.util.FileNameMapper; +import org.apache.tools.ant.util.FileUtils; +import org.apache.tools.ant.util.GlobPatternMapper; +import org.apache.tools.ant.util.IdentityMapper; +import org.apache.tools.ant.util.MergingMapper; +import org.apache.tools.ant.util.ResourceUtils; +import org.apache.tools.zip.UnixStat; +import org.apache.tools.zip.Zip64Mode; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipExtraField; +import org.apache.tools.zip.ZipFile; +import org.apache.tools.zip.ZipOutputStream; +import org.apache.tools.zip.ZipOutputStream.UnicodeExtraFieldPolicy; + +/** + * Create a Zip file. + * + * @since Ant 1.1 + * + * @ant.task category="packaging" + */ +public class Zip extends MatchingTask { + private static final int BUFFER_SIZE = 8 * 1024; + private static final int ROUNDUP_MILLIS = 1999; // 2 seconds - 1 + // CheckStyle:VisibilityModifier OFF - bc + + protected File zipFile; + // use to scan own archive + private ZipScanner zs; + private File baseDir; + protected Hashtable<String, String> entries = new Hashtable<String, String>(); + private final Vector<FileSet> groupfilesets = new Vector<FileSet>(); + private final Vector<ZipFileSet> filesetsFromGroupfilesets = new Vector<ZipFileSet>(); + protected String duplicate = "add"; + private boolean doCompress = true; + private boolean doUpdate = false; + // shadow of the above if the value is altered in execute + private boolean savedDoUpdate = false; + private boolean doFilesonly = false; + protected String archiveType = "zip"; + + // For directories: + private static final long EMPTY_CRC = new CRC32 ().getValue (); + protected String emptyBehavior = "skip"; + private final Vector<ResourceCollection> resources = new Vector<ResourceCollection>(); + protected Hashtable<String, String> addedDirs = new Hashtable<String, String>(); + private final Vector<String> addedFiles = new Vector<String>(); + + private static final ResourceSelector MISSING_SELECTOR = + new ResourceSelector() { + public boolean isSelected(final Resource target) { + return !target.isExists(); + } + }; + + private static final ResourceUtils.ResourceSelectorProvider + MISSING_DIR_PROVIDER = new ResourceUtils.ResourceSelectorProvider() { + public ResourceSelector + getTargetSelectorForSource(final Resource sr) { + return MISSING_SELECTOR; + } + }; + + /** + * If this flag is true, execute() will run most operations twice, + * the first time with {@link #skipWriting skipWriting} set to + * true and the second time with setting it to false. + * + * <p>The only situation in Ant's current code base where this is + * ever going to be true is if the jar task has been configured + * with a filesetmanifest other than "skip".</p> + */ + protected boolean doubleFilePass = false; + /** + * whether the methods should just perform some sort of dry-run. + * + * <p>Will only ever be true in the first pass if the task + * performs two passes because {@link #doubleFilePass + * doubleFilePass} is true.</p> + */ + protected boolean skipWriting = false; + + /** + * Whether this is the first time the archive building methods are invoked. + * + * @return true if either {@link #doubleFilePass doubleFilePass} + * is false or {@link #skipWriting skipWriting} is true. + * + * @since Ant 1.8.0 + */ + protected final boolean isFirstPass() { + return !doubleFilePass || skipWriting; + } + + private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); + + // CheckStyle:VisibilityModifier ON + + // This boolean is set if the task detects that the + // target is outofdate and has written to the target file. + private boolean updatedFile = false; + + /** + * true when we are adding new files into the Zip file, as opposed + * to adding back the unchanged files + */ + private boolean addingNewFiles = false; + + /** + * Encoding to use for filenames, defaults to the platform's + * default encoding. + */ + private String encoding; + + /** + * Whether the original compression of entries coming from a ZIP + * archive should be kept (for example when updating an archive). + * + * @since Ant 1.6 + */ + private boolean keepCompression = false; + + /** + * Whether the file modification times will be rounded up to the + * next even number of seconds. + * + * @since Ant 1.6.2 + */ + private boolean roundUp = true; + + /** + * Comment for the archive. + * @since Ant 1.6.3 + */ + private String comment = ""; + + private int level = ZipOutputStream.DEFAULT_COMPRESSION; + + /** + * Assume 0 Unix mode is intentional. + * @since Ant 1.8.0 + */ + private boolean preserve0Permissions = false; + + /** + * Whether to set the language encoding flag when creating the archive. + * + * @since Ant 1.8.0 + */ + private boolean useLanguageEncodingFlag = true; + + /** + * Whether to add unicode extra fields. + * + * @since Ant 1.8.0 + */ + private UnicodeExtraField createUnicodeExtraFields = + UnicodeExtraField.NEVER; + + /** + * Whether to fall back to UTF-8 if a name cannot be encoded using + * the specified encoding. + * + * @since Ant 1.8.0 + */ + private boolean fallBackToUTF8 = false; + + /** + * Whether to enable Zip64 extensions. + * + * @since Ant 1.9.1 + */ + private Zip64ModeAttribute zip64Mode = Zip64ModeAttribute.AS_NEEDED; + + /** + * This is the name/location of where to + * create the .zip file. + * @param zipFile the path of the zipFile + * @deprecated since 1.5.x. + * Use setDestFile(File) instead. + * @ant.attribute ignore="true" + */ + @Deprecated + public void setZipfile(final File zipFile) { + setDestFile(zipFile); + } + + /** + * This is the name/location of where to + * create the file. + * @param file the path of the zipFile + * @since Ant 1.5 + * @deprecated since 1.5.x. + * Use setDestFile(File) instead. + * @ant.attribute ignore="true" + */ + @Deprecated + public void setFile(final File file) { + setDestFile(file); + } + + + /** + * The file to create; required. + * @since Ant 1.5 + * @param destFile The new destination File + */ + public void setDestFile(final File destFile) { + this.zipFile = destFile; + } + + /** + * The file to create. + * @return the destination file + * @since Ant 1.5.2 + */ + public File getDestFile() { + return zipFile; + } + + + /** + * Directory from which to archive files; optional. + * @param baseDir the base directory + */ + public void setBasedir(final File baseDir) { + this.baseDir = baseDir; + } + + /** + * Whether we want to compress the files or only store them; + * optional, default=true; + * @param c if true, compress the files + */ + public void setCompress(final boolean c) { + doCompress = c; + } + + /** + * Whether we want to compress the files or only store them; + * @return true if the files are to be compressed + * @since Ant 1.5.2 + */ + public boolean isCompress() { + return doCompress; + } + + /** + * If true, emulate Sun's jar utility by not adding parent directories; + * optional, defaults to false. + * @param f if true, emulate sun's jar by not adding parent directories + */ + public void setFilesonly(final boolean f) { + doFilesonly = f; + } + + /** + * If true, updates an existing file, otherwise overwrite + * any existing one; optional defaults to false. + * @param c if true, updates an existing zip file + */ + public void setUpdate(final boolean c) { + doUpdate = c; + savedDoUpdate = c; + } + + /** + * Are we updating an existing archive? + * @return true if updating an existing archive + */ + public boolean isInUpdateMode() { + return doUpdate; + } + + /** + * Adds a set of files. + * @param set the fileset to add + */ + public void addFileset(final FileSet set) { + add(set); + } + + /** + * Adds a set of files that can be + * read from an archive and be given a prefix/fullpath. + * @param set the zipfileset to add + */ + public void addZipfileset(final ZipFileSet set) { + add(set); + } + + /** + * Add a collection of resources to be archived. + * @param a the resources to archive + * @since Ant 1.7 + */ + public void add(final ResourceCollection a) { + resources.add(a); + } + + /** + * Adds a group of zip files. + * @param set the group (a fileset) to add + */ + public void addZipGroupFileset(final FileSet set) { + groupfilesets.addElement(set); + } + + /** + * Sets behavior for when a duplicate file is about to be added - + * one of <code>add</code>, <code>preserve</code> or <code>fail</code>. + * Possible values are: <code>add</code> (keep both + * of the files); <code>preserve</code> (keep the first version + * of the file found); <code>fail</code> halt a problem + * Default for zip tasks is <code>add</code> + * @param df a <code>Duplicate</code> enumerated value + */ + public void setDuplicate(final Duplicate df) { + duplicate = df.getValue(); + } + + /** + * Possible behaviors when there are no matching files for the task: + * "fail", "skip", or "create". + */ + public static class WhenEmpty extends EnumeratedAttribute { + /** + * The string values for the enumerated value + * @return the values + */ + @Override + public String[] getValues() { + return new String[] {"fail", "skip", "create"}; + } + } + + /** + * Sets behavior of the task when no files match. + * Possible values are: <code>fail</code> (throw an exception + * and halt the build); <code>skip</code> (do not create + * any archive, but issue a warning); <code>create</code> + * (make an archive with no entries). + * Default for zip tasks is <code>skip</code>; + * for jar tasks, <code>create</code>. + * @param we a <code>WhenEmpty</code> enumerated value + */ + public void setWhenempty(final WhenEmpty we) { + emptyBehavior = we.getValue(); + } + + /** + * Encoding to use for filenames, defaults to the platform's + * default encoding. + * + * <p>For a list of possible values see <a + * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.</p> + * @param encoding the encoding name + */ + public void setEncoding(final String encoding) { + this.encoding = encoding; + } + + /** + * Encoding to use for filenames. + * @return the name of the encoding to use + * @since Ant 1.5.2 + */ + public String getEncoding() { + return encoding; + } + + /** + * Whether the original compression of entries coming from a ZIP + * archive should be kept (for example when updating an archive). + * Default is false. + * @param keep if true, keep the original compression + * @since Ant 1.6 + */ + public void setKeepCompression(final boolean keep) { + keepCompression = keep; + } + + /** + * Comment to use for archive. + * + * @param comment The content of the comment. + * @since Ant 1.6.3 + */ + public void setComment(final String comment) { + this.comment = comment; + } + + /** + * Comment of the archive + * + * @return Comment of the archive. + * @since Ant 1.6.3 + */ + public String getComment() { + return comment; + } + + /** + * Set the compression level to use. Default is + * ZipOutputStream.DEFAULT_COMPRESSION. + * @param level compression level. + * @since Ant 1.7 + */ + public void setLevel(final int level) { + this.level = level; + } + + /** + * Get the compression level. + * @return compression level. + * @since Ant 1.7 + */ + public int getLevel() { + return level; + } + + /** + * Whether the file modification times will be rounded up to the + * next even number of seconds. + * + * <p>Zip archives store file modification times with a + * granularity of two seconds, so the times will either be rounded + * up or down. If you round down, the archive will always seem + * out-of-date when you rerun the task, so the default is to round + * up. Rounding up may lead to a different type of problems like + * JSPs inside a web archive that seem to be slightly more recent + * than precompiled pages, rendering precompilation useless.</p> + * @param r a <code>boolean</code> value + * @since Ant 1.6.2 + */ + public void setRoundUp(final boolean r) { + roundUp = r; + } + + /** + * Assume 0 Unix mode is intentional. + * @since Ant 1.8.0 + */ + public void setPreserve0Permissions(final boolean b) { + preserve0Permissions = b; + } + + /** + * Assume 0 Unix mode is intentional. + * @since Ant 1.8.0 + */ + public boolean getPreserve0Permissions() { + return preserve0Permissions; + } + + /** + * Whether to set the language encoding flag. + * @since Ant 1.8.0 + */ + public void setUseLanguageEncodingFlag(final boolean b) { + useLanguageEncodingFlag = b; + } + + /** + * Whether the language encoding flag will be used. + * @since Ant 1.8.0 + */ + public boolean getUseLanguageEnodingFlag() { + return useLanguageEncodingFlag; + } + + /** + * Whether Unicode extra fields will be created. + * @since Ant 1.8.0 + */ + public void setCreateUnicodeExtraFields(final UnicodeExtraField b) { + createUnicodeExtraFields = b; + } + + /** + * Whether Unicode extra fields will be created. + * @since Ant 1.8.0 + */ + public UnicodeExtraField getCreateUnicodeExtraFields() { + return createUnicodeExtraFields; + } + + /** + * Whether to fall back to UTF-8 if a name cannot be encoded using + * the specified encoding. + * + * <p>Defaults to false.</p> + * + * @since Ant 1.8.0 + */ + public void setFallBackToUTF8(final boolean b) { + fallBackToUTF8 = b; + } + + /** + * Whether to fall back to UTF-8 if a name cannot be encoded using + * the specified encoding. + * + * @since Ant 1.8.0 + */ + public boolean getFallBackToUTF8() { + return fallBackToUTF8; + } + + /** + * Whether Zip64 extensions should be used. + * @since Ant 1.9.1 + */ + public void setZip64Mode(final Zip64ModeAttribute b) { + zip64Mode = b; + } + + /** + * Whether Zip64 extensions will be used. + * @since Ant 1.9.1 + */ + public Zip64ModeAttribute getZip64Mode() { + return zip64Mode; + } + + /** + * validate and build + * @throws BuildException on error + */ + @Override + public void execute() throws BuildException { + + if (doubleFilePass) { + skipWriting = true; + executeMain(); + skipWriting = false; + executeMain(); + } else { + executeMain(); + } + } + + /** + * Get the value of the updatedFile attribute. + * This should only be called after executeMain has been + * called. + * @return true if executeMain has written to the zip file. + */ + protected boolean hasUpdatedFile() { + return updatedFile; + } + + /** + * Build the zip file. + * This is called twice if doubleFilePass is true. + * @throws BuildException on error + */ + public void executeMain() throws BuildException { + + checkAttributesAndElements(); + + // Renamed version of original file, if it exists + File renamedFile = null; + addingNewFiles = true; + + processDoUpdate(); + processGroupFilesets(); + + // collect filesets to pass them to getResourcesToAdd + final Vector<ResourceCollection> vfss = new Vector<ResourceCollection>(); + if (baseDir != null) { + final FileSet fs = (FileSet) getImplicitFileSet().clone(); + fs.setDir(baseDir); + vfss.addElement(fs); + } + final int size = resources.size(); + for (int i = 0; i < size; i++) { + final ResourceCollection rc = resources.elementAt(i); + vfss.addElement(rc); + } + + final ResourceCollection[] fss = new ResourceCollection[vfss.size()]; + vfss.copyInto(fss); + boolean success = false; + try { + // can also handle empty archives + final ArchiveState state = getResourcesToAdd(fss, zipFile, false); + + // quick exit if the target is up to date + if (!state.isOutOfDate()) { + return; + } + + final File parent = zipFile.getParentFile(); + if (parent != null && !parent.isDirectory() + && !(parent.mkdirs() || parent.isDirectory())) { + throw new BuildException("Failed to create missing parent" + + " directory for " + zipFile); + } + + updatedFile = true; + if (!zipFile.exists() && state.isWithoutAnyResources()) { + createEmptyZip(zipFile); + return; + } + final Resource[][] addThem = state.getResourcesToAdd(); + + if (doUpdate) { + renamedFile = renameFile(); + } + + final String action = doUpdate ? "Updating " : "Building "; + + if (!skipWriting) { + log(action + archiveType + ": " + zipFile.getAbsolutePath()); + } + + ZipOutputStream zOut = null; + try { + if (!skipWriting) { + zOut = new ZipOutputStream(zipFile); + + zOut.setEncoding(encoding); + zOut.setUseLanguageEncodingFlag(useLanguageEncodingFlag); + zOut.setCreateUnicodeExtraFields(createUnicodeExtraFields. + getPolicy()); + zOut.setFallbackToUTF8(fallBackToUTF8); + zOut.setMethod(doCompress + ? ZipOutputStream.DEFLATED : ZipOutputStream.STORED); + zOut.setLevel(level); + zOut.setUseZip64(zip64Mode.getMode()); + } + initZipOutputStream(zOut); + + // Add the explicit resource collections to the archive. + for (int i = 0; i < fss.length; i++) { + if (addThem[i].length != 0) { + addResources(fss[i], addThem[i], zOut); + } + } + + if (doUpdate) { + addingNewFiles = false; + final ZipFileSet oldFiles = new ZipFileSet(); + oldFiles.setProject(getProject()); + oldFiles.setSrc(renamedFile); + oldFiles.setDefaultexcludes(false); + + final int addSize = addedFiles.size(); + for (int i = 0; i < addSize; i++) { + final PatternSet.NameEntry ne = oldFiles.createExclude(); + ne.setName(addedFiles.elementAt(i)); + } + final DirectoryScanner ds = + oldFiles.getDirectoryScanner(getProject()); + ((ZipScanner) ds).setEncoding(encoding); + + final String[] f = ds.getIncludedFiles(); + Resource[] r = new Resource[f.length]; + for (int i = 0; i < f.length; i++) { + r[i] = ds.getResource(f[i]); + } + + if (!doFilesonly) { + final String[] d = ds.getIncludedDirectories(); + final Resource[] dr = new Resource[d.length]; + for (int i = 0; i < d.length; i++) { + dr[i] = ds.getResource(d[i]); + } + final Resource[] tmp = r; + r = new Resource[tmp.length + dr.length]; + System.arraycopy(dr, 0, r, 0, dr.length); + System.arraycopy(tmp, 0, r, dr.length, tmp.length); + } + addResources(oldFiles, r, zOut); + } + if (zOut != null) { + zOut.setComment(comment); + } + finalizeZipOutputStream(zOut); + + // If we've been successful on an update, delete the + // temporary file + if (doUpdate) { + if (!renamedFile.delete()) { + log ("Warning: unable to delete temporary file " + + renamedFile.getName(), Project.MSG_WARN); + } + } + success = true; + } finally { + // Close the output stream. + closeZout(zOut, success); + } + } catch (final IOException ioe) { + String msg = "Problem creating " + archiveType + ": " + + ioe.getMessage(); + + // delete a bogus ZIP file (but only if it's not the original one) + if ((!doUpdate || renamedFile != null) && !zipFile.delete()) { + msg += " (and the archive is probably corrupt but I could not " + + "delete it)"; + } + + if (doUpdate && renamedFile != null) { + try { + FILE_UTILS.rename(renamedFile, zipFile); + } catch (final IOException e) { + msg += " (and I couldn't rename the temporary file " + + renamedFile.getName() + " back)"; + } + } + + throw new BuildException(msg, ioe, getLocation()); + } finally { + cleanUp(); + } + } + + /** rename the zip file. */ + private File renameFile() { + final File renamedFile = FILE_UTILS.createTempFile( + "zip", ".tmp", zipFile.getParentFile(), true, false); + try { + FILE_UTILS.rename(zipFile, renamedFile); + } catch (final SecurityException e) { + throw new BuildException( + "Not allowed to rename old file (" + + zipFile.getAbsolutePath() + + ") to temporary file"); + } catch (final IOException e) { + throw new BuildException( + "Unable to rename old file (" + + zipFile.getAbsolutePath() + + ") to temporary file"); + } + return renamedFile; + } + + /** Close zout */ + private void closeZout(final ZipOutputStream zOut, final boolean success) + throws IOException { + if (zOut == null) { + return; + } + try { + zOut.close(); + } catch (final IOException ex) { + // If we're in this finally clause because of an + // exception, we don't really care if there's an + // exception when closing the stream. E.g. if it + // throws "ZIP file must have at least one entry", + // because an exception happened before we added + // any files, then we must swallow this + // exception. Otherwise, the error that's reported + // will be the close() error, which is not the + // real cause of the problem. + if (success) { + throw ex; + } + } + } + + /** Check the attributes and elements */ + private void checkAttributesAndElements() { + if (baseDir == null && resources.size() == 0 + && groupfilesets.size() == 0 && "zip".equals(archiveType)) { + throw new BuildException("basedir attribute must be set, " + + "or at least one " + + "resource collection must be given!"); + } + + if (zipFile == null) { + throw new BuildException("You must specify the " + + archiveType + " file to create!"); + } + + if (zipFile.exists() && !zipFile.isFile()) { + throw new BuildException(zipFile + " is not a file."); + } + + if (zipFile.exists() && !zipFile.canWrite()) { + throw new BuildException(zipFile + " is read-only."); + } + } + + /** Process doupdate */ + private void processDoUpdate() { + // Whether or not an actual update is required - + // we don't need to update if the original file doesn't exist + if (doUpdate && !zipFile.exists()) { + doUpdate = false; + logWhenWriting("ignoring update attribute as " + archiveType + + " doesn't exist.", Project.MSG_DEBUG); + } + } + + /** Process groupfilesets */ + private void processGroupFilesets() { + // Add the files found in groupfileset to fileset + final int size = groupfilesets.size(); + for (int i = 0; i < size; i++) { + + logWhenWriting("Processing groupfileset ", Project.MSG_VERBOSE); + final FileSet fs = groupfilesets.elementAt(i); + final FileScanner scanner = fs.getDirectoryScanner(getProject()); + final String[] files = scanner.getIncludedFiles(); + final File basedir = scanner.getBasedir(); + for (int j = 0; j < files.length; j++) { + + logWhenWriting("Adding file " + files[j] + " to fileset", + Project.MSG_VERBOSE); + final ZipFileSet zf = new ZipFileSet(); + zf.setProject(getProject()); + zf.setSrc(new File(basedir, files[j])); + add(zf); + filesetsFromGroupfilesets.addElement(zf); + } + } + } + + /** + * Indicates if the task is adding new files into the archive as opposed to + * copying back unchanged files from the backup copy + * @return true if adding new files + */ + protected final boolean isAddingNewFiles() { + return addingNewFiles; + } + + /** + * Add the given resources. + * + * @param fileset may give additional information like fullpath or + * permissions. + * @param resources the resources to add + * @param zOut the stream to write to + * @throws IOException on error + * + * @since Ant 1.5.2 + */ + protected final void addResources(final FileSet fileset, final Resource[] resources, + final ZipOutputStream zOut) + throws IOException { + + String prefix = ""; + String fullpath = ""; + int dirMode = ArchiveFileSet.DEFAULT_DIR_MODE; + int fileMode = ArchiveFileSet.DEFAULT_FILE_MODE; + + ArchiveFileSet zfs = null; + if (fileset instanceof ArchiveFileSet) { + zfs = (ArchiveFileSet) fileset; + prefix = zfs.getPrefix(getProject()); + fullpath = zfs.getFullpath(getProject()); + dirMode = zfs.getDirMode(getProject()); + fileMode = zfs.getFileMode(getProject()); + } + + if (prefix.length() > 0 && fullpath.length() > 0) { + throw new BuildException("Both prefix and fullpath attributes must" + + " not be set on the same fileset."); + } + + if (resources.length != 1 && fullpath.length() > 0) { + throw new BuildException("fullpath attribute may only be specified" + + " for filesets that specify a single" + + " file."); + } + + if (prefix.length() > 0) { + if (!prefix.endsWith("/") && !prefix.endsWith("\\")) { + prefix += "/"; + } + addParentDirs(null, prefix, zOut, "", dirMode); + } + + ZipFile zf = null; + try { + boolean dealingWithFiles = false; + File base = null; + + if (zfs == null || zfs.getSrc(getProject()) == null) { + dealingWithFiles = true; + base = fileset.getDir(getProject()); + } else if (zfs instanceof ZipFileSet) { + zf = new ZipFile(zfs.getSrc(getProject()), encoding); + } + + for (int i = 0; i < resources.length; i++) { + String name = null; + if (fullpath.length() > 0) { + name = fullpath; + } else { + name = resources[i].getName(); + } + name = name.replace(File.separatorChar, '/'); + + if ("".equals(name)) { + continue; + } + + if (resources[i].isDirectory()) { + if (doFilesonly) { + continue; + } + final int thisDirMode = zfs != null && zfs.hasDirModeBeenSet() + ? dirMode : getUnixMode(resources[i], zf, dirMode); + addDirectoryResource(resources[i], name, prefix, + base, zOut, + dirMode, thisDirMode); + + } else { // !isDirectory + + addParentDirs(base, name, zOut, prefix, dirMode); + + if (dealingWithFiles) { + final File f = FILE_UTILS.resolveFile(base, + resources[i].getName()); + zipFile(f, zOut, prefix + name, fileMode); + } else { + final int thisFileMode = + zfs != null && zfs.hasFileModeBeenSet() + ? fileMode : getUnixMode(resources[i], zf, + fileMode); + addResource(resources[i], name, prefix, + zOut, thisFileMode, zf, + zfs == null + ? null : zfs.getSrc(getProject())); + } + } + } + } finally { + if (zf != null) { + zf.close(); + } + } + } + + /** + * Add a directory entry to the archive using a specified + * Unix-mode and the default mode for its parent directories (if + * necessary). + */ + private void addDirectoryResource(final Resource r, String name, final String prefix, + final File base, final ZipOutputStream zOut, + final int defaultDirMode, final int thisDirMode) + throws IOException { + + if (!name.endsWith("/")) { + name = name + "/"; + } + + final int nextToLastSlash = name.lastIndexOf("/", name.length() - 2); + if (nextToLastSlash != -1) { + addParentDirs(base, name.substring(0, nextToLastSlash + 1), + zOut, prefix, defaultDirMode); + } + zipDir(r, zOut, prefix + name, thisDirMode, + r instanceof ZipResource + ? ((ZipResource) r).getExtraFields() : null); + } + + /** + * Determine a Resource's Unix mode or return the given default + * value if not available. + */ + private int getUnixMode(final Resource r, final ZipFile zf, final int defaultMode) + throws IOException { + + int unixMode = defaultMode; + if (zf != null) { + final ZipEntry ze = zf.getEntry(r.getName()); + unixMode = ze.getUnixMode(); + if ((unixMode == 0 || unixMode == UnixStat.DIR_FLAG) + && !preserve0Permissions) { + unixMode = defaultMode; + } + } else if (r instanceof ArchiveResource) { + unixMode = ((ArchiveResource) r).getMode(); + } + return unixMode; + } + + /** + * Add a file entry. + */ + private void addResource(final Resource r, final String name, final String prefix, + final ZipOutputStream zOut, final int mode, + final ZipFile zf, final File fromArchive) + throws IOException { + + if (zf != null) { + final ZipEntry ze = zf.getEntry(r.getName()); + + if (ze != null) { + final boolean oldCompress = doCompress; + if (keepCompression) { + doCompress = (ze.getMethod() == ZipEntry.DEFLATED); + } + InputStream is = null; + try { + is = zf.getInputStream(ze); + zipFile(is, zOut, prefix + name, ze.getTime(), + fromArchive, mode, ze.getExtraFields(true)); + } finally { + doCompress = oldCompress; + FileUtils.close(is); + } + } + } else { + InputStream is = null; + try { + is = r.getInputStream(); + zipFile(is, zOut, prefix + name, r.getLastModified(), + fromArchive, mode, r instanceof ZipResource + ? ((ZipResource) r).getExtraFields() : null); + } finally { + FileUtils.close(is); + } + } + } + + /** + * Add the given resources. + * + * @param rc may give additional information like fullpath or + * permissions. + * @param resources the resources to add + * @param zOut the stream to write to + * @throws IOException on error + * + * @since Ant 1.7 + */ + protected final void addResources(final ResourceCollection rc, + final Resource[] resources, + final ZipOutputStream zOut) + throws IOException { + if (rc instanceof FileSet) { + addResources((FileSet) rc, resources, zOut); + return; + } + for (int i = 0; i < resources.length; i++) { + final Resource resource = resources[i]; + String name = resource.getName(); + if (name == null) { + continue; + } + name = name.replace(File.separatorChar, '/'); + + if ("".equals(name)) { + continue; + } + if (resource.isDirectory() && doFilesonly) { + continue; + } + File base = null; + final FileProvider fp = resource.as(FileProvider.class); + if (fp != null) { + base = ResourceUtils.asFileResource(fp).getBaseDir(); + } + + if (resource.isDirectory()) { + addDirectoryResource(resource, name, "", base, zOut, + ArchiveFileSet.DEFAULT_DIR_MODE, + ArchiveFileSet.DEFAULT_DIR_MODE); + + } else { + addParentDirs(base, name, zOut, "", + ArchiveFileSet.DEFAULT_DIR_MODE); + + if (fp != null) { + final File f = (fp).getFile(); + zipFile(f, zOut, name, ArchiveFileSet.DEFAULT_FILE_MODE); + } else { + addResource(resource, name, "", zOut, + ArchiveFileSet.DEFAULT_FILE_MODE, + null, null); + } + } + } + } + + /** + * method for subclasses to override + * @param zOut the zip output stream + * @throws IOException on output error + * @throws BuildException on other errors + */ + protected void initZipOutputStream(final ZipOutputStream zOut) + throws IOException, BuildException { + } + + /** + * method for subclasses to override + * @param zOut the zip output stream + * @throws IOException on output error + * @throws BuildException on other errors + */ + protected void finalizeZipOutputStream(final ZipOutputStream zOut) + throws IOException, BuildException { + } + + /** + * Create an empty zip file + * @param zipFile the zip file + * @return true for historic reasons + * @throws BuildException on error + */ + protected boolean createEmptyZip(final File zipFile) throws BuildException { + // In this case using java.util.zip will not work + // because it does not permit a zero-entry archive. + // Must create it manually. + if (!skipWriting) { + log("Note: creating empty " + archiveType + " archive " + zipFile, + Project.MSG_INFO); + } + OutputStream os = null; + try { + os = new FileOutputStream(zipFile); + // CheckStyle:MagicNumber OFF + // Cf. PKZIP specification. + final byte[] empty = new byte[22]; + empty[0] = 80; // P + empty[1] = 75; // K + empty[2] = 5; + empty[3] = 6; + // remainder zeros + // CheckStyle:MagicNumber ON + os.write(empty); + } catch (final IOException ioe) { + throw new BuildException("Could not create empty ZIP archive " + + "(" + ioe.getMessage() + ")", ioe, + getLocation()); + } finally { + FileUtils.close(os); + } + return true; + } + + /** + * @since Ant 1.5.2 + */ + private synchronized ZipScanner getZipScanner() { + if (zs == null) { + zs = new ZipScanner(); + zs.setEncoding(encoding); + zs.setSrc(zipFile); + } + return zs; + } + + /** + * Collect the resources that are newer than the corresponding + * entries (or missing) in the original archive. + * + * <p>If we are going to recreate the archive instead of updating + * it, all resources should be considered as new, if a single one + * is. Because of this, subclasses overriding this method must + * call <code>super.getResourcesToAdd</code> and indicate with the + * third arg if they already know that the archive is + * out-of-date.</p> + * + * <p>This method first delegates to getNonFileSetResourcesToAdd + * and then invokes the FileSet-arg version. All this to keep + * backwards compatibility for subclasses that don't know how to + * deal with non-FileSet ResourceCollections.</p> + * + * @param rcs The resource collections to grab resources from + * @param zipFile intended archive file (may or may not exist) + * @param needsUpdate whether we already know that the archive is + * out-of-date. Subclasses overriding this method are supposed to + * set this value correctly in their call to + * <code>super.getResourcesToAdd</code>. + * @return an array of resources to add for each fileset passed in as well + * as a flag that indicates whether the archive is uptodate. + * + * @exception BuildException if it likes + * @since Ant 1.7 + */ + protected ArchiveState getResourcesToAdd(final ResourceCollection[] rcs, + final File zipFile, + final boolean needsUpdate) + throws BuildException { + final ArrayList<ResourceCollection> filesets = new ArrayList<ResourceCollection>(); + final ArrayList<ResourceCollection> rest = new ArrayList<ResourceCollection>(); + for (int i = 0; i < rcs.length; i++) { + if (rcs[i] instanceof FileSet) { + filesets.add(rcs[i]); + } else { + rest.add(rcs[i]); + } + } + final ResourceCollection[] rc = + rest.toArray(new ResourceCollection[rest.size()]); + ArchiveState as = getNonFileSetResourcesToAdd(rc, zipFile, + needsUpdate); + + final FileSet[] fs = filesets.toArray(new FileSet[filesets + .size()]); + final ArchiveState as2 = getResourcesToAdd(fs, zipFile, as.isOutOfDate()); + if (!as.isOutOfDate() && as2.isOutOfDate()) { + /* + * Bad luck. + * + * There are resources in the filesets that make the + * archive out of date, but not in the non-fileset + * resources. We need to rescan the non-FileSets to grab + * all of them now. + */ + as = getNonFileSetResourcesToAdd(rc, zipFile, true); + } + + final Resource[][] toAdd = new Resource[rcs.length][]; + int fsIndex = 0; + int restIndex = 0; + for (int i = 0; i < rcs.length; i++) { + if (rcs[i] instanceof FileSet) { + toAdd[i] = as2.getResourcesToAdd()[fsIndex++]; + } else { + toAdd[i] = as.getResourcesToAdd()[restIndex++]; + } + } + return new ArchiveState(as2.isOutOfDate(), toAdd); + } + + /* + * This is yet another hacky construct to extend the FileSet[] + * getResourcesToAdd method so we can pass the information whether + * non-fileset resources have been available to it without having + * to move the withEmpty behavior checks (since either would break + * subclasses in several ways). + */ + private static final ThreadLocal<Boolean> HAVE_NON_FILE_SET_RESOURCES_TO_ADD = new ThreadLocal<Boolean>() { + @Override + protected Boolean initialValue() { + return Boolean.FALSE; + } + }; + + /** + * Collect the resources that are newer than the corresponding + * entries (or missing) in the original archive. + * + * <p>If we are going to recreate the archive instead of updating + * it, all resources should be considered as new, if a single one + * is. Because of this, subclasses overriding this method must + * call <code>super.getResourcesToAdd</code> and indicate with the + * third arg if they already know that the archive is + * out-of-date.</p> + * + * @param filesets The filesets to grab resources from + * @param zipFile intended archive file (may or may not exist) + * @param needsUpdate whether we already know that the archive is + * out-of-date. Subclasses overriding this method are supposed to + * set this value correctly in their call to + * <code>super.getResourcesToAdd</code>. + * @return an array of resources to add for each fileset passed in as well + * as a flag that indicates whether the archive is uptodate. + * + * @exception BuildException if it likes + */ + protected ArchiveState getResourcesToAdd(final FileSet[] filesets, + final File zipFile, + boolean needsUpdate) + throws BuildException { + + final Resource[][] initialResources = grabResources(filesets); + if (isEmpty(initialResources)) { + if (Boolean.FALSE.equals(HAVE_NON_FILE_SET_RESOURCES_TO_ADD.get())) { + if (needsUpdate && doUpdate) { + /* + * This is a rather hairy case. + * + * One of our subclasses knows that we need to + * update the archive, but at the same time, there + * are no resources known to us that would need to + * be added. Only the subclass seems to know + * what's going on. + * + * This happens if <jar> detects that the manifest + * has changed, for example. The manifest is not + * part of any resources because of our support + * for inline <manifest>s. + * + * If we invoke createEmptyZip like Ant 1.5.2 did, + * we'll loose all stuff that has been in the + * original archive (bugzilla report 17780). + */ + return new ArchiveState(true, initialResources); + } + + if (emptyBehavior.equals("skip")) { + if (doUpdate) { + logWhenWriting(archiveType + " archive " + zipFile + + " not updated because no new files were" + + " included.", Project.MSG_VERBOSE); + } else { + logWhenWriting("Warning: skipping " + archiveType + + " archive " + zipFile + + " because no files were included.", + Project.MSG_WARN); + } + } else if (emptyBehavior.equals("fail")) { + throw new BuildException("Cannot create " + archiveType + + " archive " + zipFile + + ": no files were included.", + getLocation()); + } else { + // Create. + if (!zipFile.exists()) { + needsUpdate = true; + } + } + } + + // either there are non-fileset resources or we + // (re-)create the archive anyway + return new ArchiveState(needsUpdate, initialResources); + } + + // initialResources is not empty + + if (!zipFile.exists()) { + return new ArchiveState(true, initialResources); + } + + if (needsUpdate && !doUpdate) { + // we are recreating the archive, need all resources + return new ArchiveState(true, initialResources); + } + + final Resource[][] newerResources = new Resource[filesets.length][]; + + for (int i = 0; i < filesets.length; i++) { + if (!(fileset instanceof ZipFileSet) + || ((ZipFileSet) fileset).getSrc(getProject()) == null) { + final File base = filesets[i].getDir(getProject()); + + for (int j = 0; j < initialResources[i].length; j++) { + final File resourceAsFile = + FILE_UTILS.resolveFile(base, + initialResources[i][j].getName()); + if (resourceAsFile.equals(zipFile)) { + throw new BuildException("A zip file cannot include " + + "itself", getLocation()); + } + } + } + } + + for (int i = 0; i < filesets.length; i++) { + if (initialResources[i].length == 0) { + newerResources[i] = new Resource[] {}; + continue; + } + + FileNameMapper myMapper = new IdentityMapper(); + if (filesets[i] instanceof ZipFileSet) { + final ZipFileSet zfs = (ZipFileSet) filesets[i]; + if (zfs.getFullpath(getProject()) != null + && !zfs.getFullpath(getProject()).equals("")) { + // in this case all files from origin map to + // the fullPath attribute of the zipfileset at + // destination + final MergingMapper fm = new MergingMapper(); + fm.setTo(zfs.getFullpath(getProject())); + myMapper = fm; + + } else if (zfs.getPrefix(getProject()) != null + && !zfs.getPrefix(getProject()).equals("")) { + final GlobPatternMapper gm = new GlobPatternMapper(); + gm.setFrom("*"); + String prefix = zfs.getPrefix(getProject()); + if (!prefix.endsWith("/") && !prefix.endsWith("\\")) { + prefix += "/"; + } + gm.setTo(prefix + "*"); + myMapper = gm; + } + } + + newerResources[i] = selectOutOfDateResources(initialResources[i], + myMapper); + needsUpdate = needsUpdate || (newerResources[i].length > 0); + + if (needsUpdate && !doUpdate) { + // we will return initialResources anyway, no reason + // to scan further. + break; + } + } + + if (needsUpdate && !doUpdate) { + // we are recreating the archive, need all resources + return new ArchiveState(true, initialResources); + } + + return new ArchiveState(needsUpdate, newerResources); + } + + /** + * Collect the resources that are newer than the corresponding + * entries (or missing) in the original archive. + * + * <p>If we are going to recreate the archive instead of updating + * it, all resources should be considered as new, if a single one + * is. Because of this, subclasses overriding this method must + * call <code>super.getResourcesToAdd</code> and indicate with the + * third arg if they already know that the archive is + * out-of-date.</p> + * + * @param rcs The filesets to grab resources from + * @param zipFile intended archive file (may or may not exist) + * @param needsUpdate whether we already know that the archive is + * out-of-date. Subclasses overriding this method are supposed to + * set this value correctly in their call to + * <code>super.getResourcesToAdd</code>. + * @return an array of resources to add for each fileset passed in as well + * as a flag that indicates whether the archive is uptodate. + * + * @exception BuildException if it likes + */ + protected ArchiveState getNonFileSetResourcesToAdd(final ResourceCollection[] rcs, + final File zipFile, + boolean needsUpdate) + throws BuildException { + /* + * Backwards compatibility forces us to repeat the logic of + * getResourcesToAdd(FileSet[], ...) here once again. + */ + + final Resource[][] initialResources = grabNonFileSetResources(rcs); + final boolean empty = isEmpty(initialResources); + HAVE_NON_FILE_SET_RESOURCES_TO_ADD.set(Boolean.valueOf(!empty)); + if (empty) { + // no emptyBehavior handling since the FileSet version + // will take care of it. + return new ArchiveState(needsUpdate, initialResources); + } + + // initialResources is not empty + + if (!zipFile.exists()) { + return new ArchiveState(true, initialResources); + } + + if (needsUpdate && !doUpdate) { + // we are recreating the archive, need all resources + return new ArchiveState(true, initialResources); + } + + final Resource[][] newerResources = new Resource[rcs.length][]; + + for (int i = 0; i < rcs.length; i++) { + if (initialResources[i].length == 0) { + newerResources[i] = new Resource[] {}; + continue; + } + + for (int j = 0; j < initialResources[i].length; j++) { + final FileProvider fp = + initialResources[i][j].as(FileProvider.class); + if (fp != null && zipFile.equals(fp.getFile())) { + throw new BuildException("A zip file cannot include " + + "itself", getLocation()); + } + } + + newerResources[i] = selectOutOfDateResources(initialResources[i], + new IdentityMapper()); + needsUpdate = needsUpdate || (newerResources[i].length > 0); + + if (needsUpdate && !doUpdate) { + // we will return initialResources anyway, no reason + // to scan further. + break; + } + } + + if (needsUpdate && !doUpdate) { + // we are recreating the archive, need all resources + return new ArchiveState(true, initialResources); + } + + return new ArchiveState(needsUpdate, newerResources); + } + + private Resource[] selectOutOfDateResources(final Resource[] initial, + final FileNameMapper mapper) { + final Resource[] rs = selectFileResources(initial); + Resource[] result = + ResourceUtils.selectOutOfDateSources(this, rs, mapper, + getZipScanner()); + if (!doFilesonly) { + final Union u = new Union(); + u.addAll(Arrays.asList(selectDirectoryResources(initial))); + final ResourceCollection rc = + ResourceUtils.selectSources(this, u, mapper, + getZipScanner(), + MISSING_DIR_PROVIDER); + if (rc.size() > 0) { + final ArrayList<Resource> newer = new ArrayList<Resource>(); + newer.addAll(Arrays.asList(((Union) rc).listResources())); + newer.addAll(Arrays.asList(result)); + result = newer.toArray(result); + } + } + return result; + } + + /** + * Fetch all included and not excluded resources from the sets. + * + * <p>Included directories will precede included files.</p> + * @param filesets an array of filesets + * @return the resources included + * @since Ant 1.5.2 + */ + protected Resource[][] grabResources(final FileSet[] filesets) { + final Resource[][] result = new Resource[filesets.length][]; + for (int i = 0; i < filesets.length; i++) { + boolean skipEmptyNames = true; + if (filesets[i] instanceof ZipFileSet) { + final ZipFileSet zfs = (ZipFileSet) filesets[i]; + skipEmptyNames = zfs.getPrefix(getProject()).equals("") + && zfs.getFullpath(getProject()).equals(""); + } + final DirectoryScanner rs = + filesets[i].getDirectoryScanner(getProject()); + if (rs instanceof ZipScanner) { + ((ZipScanner) rs).setEncoding(encoding); + } + final Vector<Resource> resources = new Vector<Resource>(); + if (!doFilesonly) { + final String[] directories = rs.getIncludedDirectories(); + for (int j = 0; j < directories.length; j++) { + if (!"".equals(directories[j]) || !skipEmptyNames) { + resources.addElement(rs.getResource(directories[j])); + } + } + } + final String[] files = rs.getIncludedFiles(); + for (int j = 0; j < files.length; j++) { + if (!"".equals(files[j]) || !skipEmptyNames) { + resources.addElement(rs.getResource(files[j])); + } + } + + result[i] = new Resource[resources.size()]; + resources.copyInto(result[i]); + } + return result; + } + + /** + * Fetch all included and not excluded resources from the collections. + * + * <p>Included directories will precede included files.</p> + * @param rcs an array of resource collections + * @return the resources included + * @since Ant 1.7 + */ + protected Resource[][] grabNonFileSetResources(final ResourceCollection[] rcs) { + final Resource[][] result = new Resource[rcs.length][]; + for (int i = 0; i < rcs.length; i++) { + final ArrayList<Resource> dirs = new ArrayList<Resource>(); + final ArrayList<Resource> files = new ArrayList<Resource>(); + for (final Resource r : rcs[i]) { + if (r.isExists()) { + if (r.isDirectory()) { + dirs.add(r); + } else { + files.add(r); + } + } + } + // make sure directories are in alpha-order - this also + // ensures parents come before their children + Collections.sort(dirs, new Comparator<Resource>() { + public int compare(final Resource r1, final Resource r2) { + return r1.getName().compareTo(r2.getName()); + } + }); + final ArrayList<Resource> rs = new ArrayList<Resource>(dirs); + rs.addAll(files); + result[i] = rs.toArray(new Resource[rs.size()]); + } + return result; + } + + /** + * Add a directory to the zip stream. + * @param dir the directort to add to the archive + * @param zOut the stream to write to + * @param vPath the name this entry shall have in the archive + * @param mode the Unix permissions to set. + * @throws IOException on error + * @since Ant 1.5.2 + */ + protected void zipDir(final File dir, final ZipOutputStream zOut, final String vPath, + final int mode) + throws IOException { + zipDir(dir, zOut, vPath, mode, null); + } + + /** + * Add a directory to the zip stream. + * @param dir the directory to add to the archive + * @param zOut the stream to write to + * @param vPath the name this entry shall have in the archive + * @param mode the Unix permissions to set. + * @param extra ZipExtraFields to add + * @throws IOException on error + * @since Ant 1.6.3 + */ + protected void zipDir(final File dir, final ZipOutputStream zOut, final String vPath, + final int mode, final ZipExtraField[] extra) + throws IOException { + zipDir(dir == null ? (Resource) null : new FileResource(dir), + zOut, vPath, mode, extra); + } + + /** + * Add a directory to the zip stream. + * @param dir the directory to add to the archive + * @param zOut the stream to write to + * @param vPath the name this entry shall have in the archive + * @param mode the Unix permissions to set. + * @param extra ZipExtraFields to add + * @throws IOException on error + * @since Ant 1.8.0 + */ + protected void zipDir(final Resource dir, final ZipOutputStream zOut, final String vPath, + final int mode, final ZipExtraField[] extra) + throws IOException { + if (doFilesonly) { + logWhenWriting("skipping directory " + vPath + + " for file-only archive", + Project.MSG_VERBOSE); + return; + } + if (addedDirs.get(vPath) != null) { + // don't add directories we've already added. + // no warning if we try, it is harmless in and of itself + return; + } + + logWhenWriting("adding directory " + vPath, Project.MSG_VERBOSE); + addedDirs.put(vPath, vPath); + + if (!skipWriting) { + final ZipEntry ze = new ZipEntry (vPath); + + // ZIPs store time with a granularity of 2 seconds, round up + final int millisToAdd = roundUp ? ROUNDUP_MILLIS : 0; + + if (dir != null && dir.isExists()) { + ze.setTime(dir.getLastModified() + millisToAdd); + } else { + ze.setTime(System.currentTimeMillis() + millisToAdd); + } + ze.setSize (0); + ze.setMethod (ZipEntry.STORED); + // This is faintly ridiculous: + ze.setCrc (EMPTY_CRC); + ze.setUnixMode(mode); + + if (extra != null) { + ze.setExtraFields(extra); + } + + zOut.putNextEntry(ze); + } + } + + /* + * This is a hacky construct to extend the zipFile method to + * support a new parameter (extra fields to preserve) without + * breaking subclasses that override the old method signature. + */ + private static final ThreadLocal<ZipExtraField[]> CURRENT_ZIP_EXTRA = new ThreadLocal<ZipExtraField[]>(); + + /** + * Provides the extra fields for the zip entry currently being + * added to the archive - if any. + * @since Ant 1.8.0 + */ + protected final ZipExtraField[] getCurrentExtraFields() { + return CURRENT_ZIP_EXTRA.get(); + } + + /** + * Sets the extra fields for the zip entry currently being + * added to the archive - if any. + * @since Ant 1.8.0 + */ + protected final void setCurrentExtraFields(final ZipExtraField[] extra) { + CURRENT_ZIP_EXTRA.set(extra); + } + + /** + * Adds a new entry to the archive, takes care of duplicates as well. + * + * @param in the stream to read data for the entry from. The + * caller of the method is responsible for closing the stream. + * @param zOut the stream to write to. + * @param vPath the name this entry shall have in the archive. + * @param lastModified last modification time for the entry. + * @param fromArchive the original archive we are copying this + * entry from, will be null if we are not copying from an archive. + * @param mode the Unix permissions to set. + * + * @since Ant 1.5.2 + * @throws IOException on error + */ + protected void zipFile(InputStream in, final ZipOutputStream zOut, final String vPath, + final long lastModified, final File fromArchive, final int mode) + throws IOException { + // fromArchive is used in subclasses overriding this method + + if (entries.containsKey(vPath)) { + + if (duplicate.equals("preserve")) { + logWhenWriting(vPath + " already added, skipping", + Project.MSG_INFO); + return; + } else if (duplicate.equals("fail")) { + throw new BuildException("Duplicate file " + vPath + + " was found and the duplicate " + + "attribute is 'fail'."); + } else { + // duplicate equal to add, so we continue + logWhenWriting("duplicate file " + vPath + + " found, adding.", Project.MSG_VERBOSE); + } + } else { + logWhenWriting("adding entry " + vPath, Project.MSG_VERBOSE); + } + + entries.put(vPath, vPath); + + if (!skipWriting) { + final ZipEntry ze = new ZipEntry(vPath); + ze.setTime(lastModified); + ze.setMethod(doCompress ? ZipEntry.DEFLATED : ZipEntry.STORED); + + /* + * ZipOutputStream.putNextEntry expects the ZipEntry to + * know its size and the CRC sum before you start writing + * the data when using STORED mode - unless it is seekable. + * + * This forces us to process the data twice. + */ + if (!zOut.isSeekable() && !doCompress) { + long size = 0; + final CRC32 cal = new CRC32(); + if (!in.markSupported()) { + // Store data into a byte[] + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + final byte[] buffer = new byte[BUFFER_SIZE]; + int count = 0; + do { + size += count; + cal.update(buffer, 0, count); + bos.write(buffer, 0, count); + count = in.read(buffer, 0, buffer.length); + } while (count != -1); + in = new ByteArrayInputStream(bos.toByteArray()); + + } else { + in.mark(Integer.MAX_VALUE); + final byte[] buffer = new byte[BUFFER_SIZE]; + int count = 0; + do { + size += count; + cal.update(buffer, 0, count); + count = in.read(buffer, 0, buffer.length); + } while (count != -1); + in.reset(); + } + ze.setSize(size); + ze.setCrc(cal.getValue()); + } + + ze.setUnixMode(mode); + final ZipExtraField[] extra = getCurrentExtraFields(); + if (extra != null) { + ze.setExtraFields(extra); + } + + zOut.putNextEntry(ze); + + final byte[] buffer = new byte[BUFFER_SIZE]; + int count = 0; + do { + if (count != 0) { + zOut.write(buffer, 0, count); + } + count = in.read(buffer, 0, buffer.length); + } while (count != -1); + } + addedFiles.addElement(vPath); + } + + /** + * Adds a new entry to the archive, takes care of duplicates as well. + * + * @param in the stream to read data for the entry from. The + * caller of the method is responsible for closing the stream. + * @param zOut the stream to write to. + * @param vPath the name this entry shall have in the archive. + * @param lastModified last modification time for the entry. + * @param fromArchive the original archive we are copying this + * entry from, will be null if we are not copying from an archive. + * @param mode the Unix permissions to set. + * @param extra ZipExtraFields to add + * + * @since Ant 1.8.0 + * @throws IOException on error + */ + protected final void zipFile(final InputStream in, final ZipOutputStream zOut, + final String vPath, final long lastModified, + final File fromArchive, final int mode, + final ZipExtraField[] extra) + throws IOException { + try { + setCurrentExtraFields(extra); + zipFile(in, zOut, vPath, lastModified, fromArchive, mode); + } finally { + setCurrentExtraFields(null); + } + } + + /** + * Method that gets called when adding from <code>java.io.File</code> instances. + * + * <p>This implementation delegates to the six-arg version.</p> + * + * @param file the file to add to the archive + * @param zOut the stream to write to + * @param vPath the name this entry shall have in the archive + * @param mode the Unix permissions to set. + * @throws IOException on error + * + * @since Ant 1.5.2 + */ + protected void zipFile(final File file, final ZipOutputStream zOut, final String vPath, + final int mode) + throws IOException { + if (file.equals(zipFile)) { + throw new BuildException("A zip file cannot include itself", + getLocation()); + } + + final FileInputStream fIn = new FileInputStream(file); + try { + // ZIPs store time with a granularity of 2 seconds, round up + zipFile(fIn, zOut, vPath, + file.lastModified() + (roundUp ? ROUNDUP_MILLIS : 0), + null, mode); + } finally { + fIn.close(); + } + } + + /** + * Ensure all parent dirs of a given entry have been added. + * @param baseDir the base directory to use (may be null) + * @param entry the entry name to create directories from + * @param zOut the stream to write to + * @param prefix a prefix to place on the created entries + * @param dirMode the directory mode + * @throws IOException on error + * @since Ant 1.5.2 + */ + protected final void addParentDirs(final File baseDir, final String entry, + final ZipOutputStream zOut, final String prefix, + final int dirMode) + throws IOException { + if (!doFilesonly) { + final Stack<String> directories = new Stack<String>(); + int slashPos = entry.length(); + + while ((slashPos = entry.lastIndexOf('/', slashPos - 1)) != -1) { + final String dir = entry.substring(0, slashPos + 1); + if (addedDirs.get(prefix + dir) != null) { + break; + } + directories.push(dir); + } + + while (!directories.isEmpty()) { + final String dir = directories.pop(); + File f = null; + if (baseDir != null) { + f = new File(baseDir, dir); + } else { + f = new File(dir); + } + zipDir(f, zOut, prefix + dir, dirMode); + } + } + } + + /** + * Do any clean up necessary to allow this instance to be used again. + * + * <p>When we get here, the Zip file has been closed and all we + * need to do is to reset some globals.</p> + * + * <p>This method will only reset globals that have been changed + * during execute(), it will not alter the attributes or nested + * child elements. If you want to reset the instance so that you + * can later zip a completely different set of files, you must use + * the reset method.</p> + * + * @see #reset + */ + protected void cleanUp() { + addedDirs.clear(); + addedFiles.removeAllElements(); + entries.clear(); + addingNewFiles = false; + doUpdate = savedDoUpdate; + final Enumeration<ZipFileSet> e = filesetsFromGroupfilesets.elements(); + while (e.hasMoreElements()) { + final ZipFileSet zf = e.nextElement(); + resources.removeElement(zf); + } + filesetsFromGroupfilesets.removeAllElements(); + HAVE_NON_FILE_SET_RESOURCES_TO_ADD.set(Boolean.FALSE); + } + + /** + * Makes this instance reset all attributes to their default + * values and forget all children. + * + * @since Ant 1.5 + * + * @see #cleanUp + */ + public void reset() { + resources.removeAllElements(); + zipFile = null; + baseDir = null; + groupfilesets.removeAllElements(); + duplicate = "add"; + archiveType = "zip"; + doCompress = true; + emptyBehavior = "skip"; + doUpdate = false; + doFilesonly = false; + encoding = null; + } + + /** + * Check is the resource arrays are empty. + * @param r the arrays to check + * @return true if all individual arrays are empty + * + * @since Ant 1.5.2 + */ + protected static final boolean isEmpty(final Resource[][] r) { + for (int i = 0; i < r.length; i++) { + if (r[i].length > 0) { + return false; + } + } + return true; + } + + /** + * Drops all non-file resources from the given array. + * @param orig the resources to filter + * @return the filters resources + * @since Ant 1.6 + */ + protected Resource[] selectFileResources(final Resource[] orig) { + return selectResources(orig, + new ResourceSelector() { + public boolean isSelected(final Resource r) { + if (!r.isDirectory()) { + return true; + } else if (doFilesonly) { + logWhenWriting("Ignoring directory " + + r.getName() + + " as only files will" + + " be added.", + Project.MSG_VERBOSE); + } + return false; + } + }); + } + + /** + * Drops all non-directory resources from the given array. + * @param orig the resources to filter + * @return the filters resources + * @since Ant 1.8.0 + */ + protected Resource[] selectDirectoryResources(final Resource[] orig) { + return selectResources(orig, + new ResourceSelector() { + public boolean isSelected(final Resource r) { + return r.isDirectory(); + } + }); + } + + /** + * Drops all resources from the given array that are not selected + * @param orig the resources to filter + * @return the filters resources + * @since Ant 1.8.0 + */ + protected Resource[] selectResources(final Resource[] orig, + final ResourceSelector selector) { + if (orig.length == 0) { + return orig; + } + + final ArrayList<Resource> v = new ArrayList<Resource>(orig.length); + for (int i = 0; i < orig.length; i++) { + if (selector.isSelected(orig[i])) { + v.add(orig[i]); + } + } + + if (v.size() != orig.length) { + return v.toArray(new Resource[v.size()]); + } + return orig; + } + + /** + * Logs a message at the given output level, but only if this is + * the pass that will actually create the archive. + * + * @since Ant 1.8.0 + */ + protected void logWhenWriting(final String msg, final int level) { + if (!skipWriting) { + log(msg, level); + } + } + + /** + * Possible behaviors when a duplicate file is added: + * "add", "preserve" or "fail" + */ + public static class Duplicate extends EnumeratedAttribute { + /** + * @see EnumeratedAttribute#getValues() + */ + /** {@inheritDoc} */ + @Override + public String[] getValues() { + return new String[] {"add", "preserve", "fail"}; + } + } + + /** + * Holds the up-to-date status and the out-of-date resources of + * the original archive. + * + * @since Ant 1.5.3 + */ + public static class ArchiveState { + private final boolean outOfDate; + private final Resource[][] resourcesToAdd; + + ArchiveState(final boolean state, final Resource[][] r) { + outOfDate = state; + resourcesToAdd = r; + } + + /** + * Return the outofdate status. + * @return the outofdate status + */ + public boolean isOutOfDate() { + return outOfDate; + } + + /** + * Get the resources to add. + * @return the resources to add + */ + public Resource[][] getResourcesToAdd() { + return resourcesToAdd; + } + /** + * find out if there are absolutely no resources to add + * @since Ant 1.6.3 + * @return true if there are no resources to add + */ + public boolean isWithoutAnyResources() { + if (resourcesToAdd == null) { + return true; + } + for (int counter = 0; counter < resourcesToAdd.length; counter++) { + if (resourcesToAdd[counter] != null) { + if (resourcesToAdd[counter].length > 0) { + return false; + } + } + } + return true; + } + } + + /** + * Policiy for creation of Unicode extra fields: never, always or + * not-encodeable. + * + * @since Ant 1.8.0 + */ + public static final class UnicodeExtraField extends EnumeratedAttribute { + private static final Map<String, UnicodeExtraFieldPolicy> POLICIES = new HashMap<String, UnicodeExtraFieldPolicy>(); + private static final String NEVER_KEY = "never"; + private static final String ALWAYS_KEY = "always"; + private static final String N_E_KEY = "not-encodeable"; + static { + POLICIES.put(NEVER_KEY, + ZipOutputStream.UnicodeExtraFieldPolicy.NEVER); + POLICIES.put(ALWAYS_KEY, + ZipOutputStream.UnicodeExtraFieldPolicy.ALWAYS); + POLICIES.put(N_E_KEY, + ZipOutputStream.UnicodeExtraFieldPolicy + .NOT_ENCODEABLE); + } + + @Override + public String[] getValues() { + return new String[] {NEVER_KEY, ALWAYS_KEY, N_E_KEY}; + } + + public static final UnicodeExtraField NEVER = + new UnicodeExtraField(NEVER_KEY); + + private UnicodeExtraField(final String name) { + setValue(name); + } + + public UnicodeExtraField() { + } + + public ZipOutputStream.UnicodeExtraFieldPolicy getPolicy() { + return POLICIES.get(getValue()); + } + } + + + /** + * The choices for Zip64 extensions. + * + * <p><b>never</b>: never add any Zip64 extensions. This will + * cause the task to fail if you try to add entries bigger than + * 4GB or create an archive bigger than 4GB or holding more that + * 65535 entries.</p> + * + * <p><b>as-needed</b>: create Zip64 extensions only when the + * entry's size is bigger than 4GB or one of the archive limits is + * hit. This mode also adds partial Zip64 extensions for all + * deflated entries written by Ant.</p> + * + * <p><b>always</b>: create Zip64 extensions for all entries.</p> + * + * <p><b>Note</b> some ZIP implementations don't handle Zip64 + * extensions well and others may fail if the Zip64 extra field + * data is only present inside the local file header but not the + * central directory - which is what <em>as-needed</em> may result + * in. Java5 and Microsoft Visual Studio's Extension loader are + * known to fconsider the archive broken in such cases. If you + * are targeting such an archiver uset the value <em>never</em> + * unless you know you need Zip64 extensions.</p> + * + * @since Ant 1.9.1 + */ + public static final class Zip64ModeAttribute extends EnumeratedAttribute { + private static final Map<String, Zip64Mode> MODES = new HashMap<String, Zip64Mode>(); + + private static final String NEVER_KEY = "never"; + private static final String ALWAYS_KEY = "always"; + private static final String A_N_KEY = "as-needed"; + static { + MODES.put(NEVER_KEY, Zip64Mode.Never); + MODES.put(ALWAYS_KEY, Zip64Mode.Always); + MODES.put(A_N_KEY, Zip64Mode.AsNeeded); + } + + @Override + public String[] getValues() { + return new String[] {NEVER_KEY, ALWAYS_KEY, A_N_KEY}; + } + + public static final Zip64ModeAttribute NEVER = + new Zip64ModeAttribute(NEVER_KEY); + public static final Zip64ModeAttribute AS_NEEDED = + new Zip64ModeAttribute(A_N_KEY); + + private Zip64ModeAttribute(final String name) { + setValue(name); + } + + public Zip64ModeAttribute() { + } + + public Zip64Mode getMode() { + return MODES.get(getValue()); + } + + } + } |