blob: 8e04932c1831ccd65db7e0bf5309175d83f42645 [file] [log] [blame]
/*
* 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.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
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.FileResource;
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.ZipEntry;
import org.apache.tools.zip.ZipExtraField;
import org.apache.tools.zip.ZipFile;
import org.apache.tools.zip.ZipOutputStream;
/**
* 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 entries = new Hashtable();
private Vector groupfilesets = new Vector();
private Vector filesetsFromGroupfilesets = new Vector();
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 Vector resources = new Vector();
protected Hashtable addedDirs = new Hashtable();
private Vector addedFiles = new Vector();
protected boolean doubleFilePass = false;
protected boolean skipWriting = false;
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;
/**
* 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"
*/
public void setZipfile(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"
*/
public void setFile(File file) {
setDestFile(file);
}
/**
* The file to create; required.
* @since Ant 1.5
* @param destFile The new destination File
*/
public void setDestFile(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(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(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(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(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(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(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(ResourceCollection a) {
resources.add(a);
}
/**
* Adds a group of zip files.
* @param set the group (a fileset) to add
*/
public void addZipGroupFileset(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(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
*/
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(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(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(boolean keep) {
keepCompression = keep;
}
/**
* Comment to use for archive.
*
* @param comment The content of the comment.
* @since Ant 1.6.3
*/
public void setComment(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(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(boolean r) {
roundUp = r;
}
/**
* validate and build
* @throws BuildException on error
*/
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
Vector vfss = new Vector();
if (baseDir != null) {
FileSet fs = (FileSet) getImplicitFileSet().clone();
fs.setDir(baseDir);
vfss.addElement(fs);
}
for (int i = 0; i < resources.size(); i++) {
ResourceCollection rc = (ResourceCollection) resources.elementAt(i);
vfss.addElement(rc);
}
ResourceCollection[] fss = new ResourceCollection[vfss.size()];
vfss.copyInto(fss);
boolean success = false;
try {
// can also handle empty archives
ArchiveState state = getResourcesToAdd(fss, zipFile, false);
// quick exit if the target is up to date
if (!state.isOutOfDate()) {
return;
}
updatedFile = true;
if (!zipFile.exists() && state.isWithoutAnyResources()) {
createEmptyZip(zipFile);
return;
}
Resource[][] addThem = state.getResourcesToAdd();
if (doUpdate) {
renamedFile = renameFile();
}
String action = doUpdate ? "Updating " : "Building ";
log(action + archiveType + ": " + zipFile.getAbsolutePath());
ZipOutputStream zOut = null;
try {
if (!skipWriting) {
zOut = new ZipOutputStream(zipFile);
zOut.setEncoding(encoding);
zOut.setMethod(doCompress
? ZipOutputStream.DEFLATED : ZipOutputStream.STORED);
zOut.setLevel(level);
}
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;
ZipFileSet oldFiles = new ZipFileSet();
oldFiles.setProject(getProject());
oldFiles.setSrc(renamedFile);
oldFiles.setDefaultexcludes(false);
for (int i = 0; i < addedFiles.size(); i++) {
PatternSet.NameEntry ne = oldFiles.createExclude();
ne.setName((String) addedFiles.elementAt(i));
}
DirectoryScanner ds =
oldFiles.getDirectoryScanner(getProject());
((ZipScanner) ds).setEncoding(encoding);
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) {
String[] d = ds.getIncludedDirectories();
Resource[] dr = new Resource[d.length];
for (int i = 0; i < d.length; i++) {
dr[i] = ds.getResource(d[i]);
}
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 (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 (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() {
File renamedFile = FILE_UTILS.createTempFile(
"zip", ".tmp", zipFile.getParentFile(), true, false);
try {
FILE_UTILS.rename(zipFile, renamedFile);
} catch (SecurityException e) {
throw new BuildException(
"Not allowed to rename old file ("
+ zipFile.getAbsolutePath()
+ ") to temporary file");
} catch (IOException e) {
throw new BuildException(
"Unable to rename old file ("
+ zipFile.getAbsolutePath()
+ ") to temporary file");
}
return renamedFile;
}
/** Close zout */
private void closeZout(ZipOutputStream zOut, boolean success)
throws IOException {
if (zOut == null) {
return;
}
try {
zOut.close();
} catch (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;
log("ignoring update attribute as " + archiveType
+ " doesn't exist.", Project.MSG_DEBUG);
}
}
/** Process groupfilesets */
private void processGroupFilesets() {
// Add the files found in groupfileset to fileset
for (int i = 0; i < groupfilesets.size(); i++) {
log("Processing groupfileset ", Project.MSG_VERBOSE);
FileSet fs = (FileSet) groupfilesets.elementAt(i);
FileScanner scanner = fs.getDirectoryScanner(getProject());
String[] files = scanner.getIncludedFiles();
File basedir = scanner.getBasedir();
for (int j = 0; j < files.length; j++) {
log("Adding file " + files[j] + " to fileset",
Project.MSG_VERBOSE);
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(FileSet fileset, Resource[] resources,
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() && !name.endsWith("/")) {
name = name + "/";
}
if (!doFilesonly && !dealingWithFiles
&& resources[i].isDirectory()
&& !zfs.hasDirModeBeenSet()) {
int nextToLastSlash = name.lastIndexOf("/",
name.length() - 2);
if (nextToLastSlash != -1) {
addParentDirs(base, name.substring(0,
nextToLastSlash + 1),
zOut, prefix, dirMode);
}
if (zf != null) {
ZipEntry ze = zf.getEntry(resources[i].getName());
addParentDirs(base, name, zOut, prefix,
ze.getUnixMode());
} else {
ArchiveResource tr = (ArchiveResource) resources[i];
addParentDirs(base, name, zOut, prefix,
tr.getMode());
}
} else {
addParentDirs(base, name, zOut, prefix, dirMode);
}
if (!resources[i].isDirectory() && dealingWithFiles) {
File f = FILE_UTILS.resolveFile(base,
resources[i].getName());
zipFile(f, zOut, prefix + name, fileMode);
} else if (!resources[i].isDirectory()) {
if (zf != null) {
ZipEntry ze = zf.getEntry(resources[i].getName());
if (ze != null) {
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(), zfs.getSrc(getProject()),
zfs.hasFileModeBeenSet() ? fileMode
: ze.getUnixMode());
} finally {
doCompress = oldCompress;
FileUtils.close(is);
}
}
} else {
ArchiveResource tr = (ArchiveResource) resources[i];
InputStream is = null;
try {
is = tr.getInputStream();
zipFile(is, zOut, prefix + name,
resources[i].getLastModified(),
zfs.getSrc(getProject()),
zfs.hasFileModeBeenSet() ? fileMode
: tr.getMode());
} finally {
FileUtils.close(is);
}
}
}
}
} finally {
if (zf != null) {
zf.close();
}
}
}
/**
* 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(ResourceCollection rc,
Resource[] resources,
ZipOutputStream zOut)
throws IOException {
if (rc instanceof FileSet) {
addResources((FileSet) rc, resources, zOut);
return;
}
for (int i = 0; i < resources.length; i++) {
String name = resources[i].getName().replace(File.separatorChar,
'/');
if ("".equals(name)) {
continue;
}
if (resources[i].isDirectory() && doFilesonly) {
continue;
}
File base = null;
if (resources[i] instanceof FileResource) {
base = ((FileResource) resources[i]).getBaseDir();
}
if (resources[i].isDirectory()) {
if (!name.endsWith("/")) {
name = name + "/";
}
}
addParentDirs(base, name, zOut, "",
ArchiveFileSet.DEFAULT_DIR_MODE);
if (!resources[i].isDirectory()) {
if (resources[i] instanceof FileResource) {
File f = ((FileResource) resources[i]).getFile();
zipFile(f, zOut, name, ArchiveFileSet.DEFAULT_FILE_MODE);
} else {
InputStream is = null;
try {
is = resources[i].getInputStream();
zipFile(is, zOut, name,
resources[i].getLastModified(),
null, ArchiveFileSet.DEFAULT_FILE_MODE);
} finally {
FileUtils.close(is);
}
}
}
}
}
/**
* method for subclasses to override
* @param zOut the zip output stream
* @throws IOException on output error
* @throws BuildException on other errors
*/
protected void initZipOutputStream(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(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(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.
log("Note: creating empty " + archiveType + " archive " + zipFile,
Project.MSG_INFO);
OutputStream os = null;
try {
os = new FileOutputStream(zipFile);
// CheckStyle:MagicNumber OFF
// Cf. PKZIP specification.
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 (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 getNonFileSetResourceToAdd
* 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(ResourceCollection[] rcs,
File zipFile,
boolean needsUpdate)
throws BuildException {
ArrayList filesets = new ArrayList();
ArrayList rest = new ArrayList();
for (int i = 0; i < rcs.length; i++) {
if (rcs[i] instanceof FileSet) {
filesets.add(rcs[i]);
} else {
rest.add(rcs[i]);
}
}
ResourceCollection[] rc = (ResourceCollection[])
rest.toArray(new ResourceCollection[rest.size()]);
ArchiveState as = getNonFileSetResourcesToAdd(rc, zipFile,
needsUpdate);
FileSet[] fs = (FileSet[]) filesets.toArray(new FileSet[filesets
.size()]);
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);
}
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);
}
/**
* 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(FileSet[] filesets,
File zipFile,
boolean needsUpdate)
throws BuildException {
Resource[][] initialResources = grabResources(filesets);
if (isEmpty(initialResources)) {
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) {
log(archiveType + " archive " + zipFile
+ " not updated because no new files were included.",
Project.MSG_VERBOSE);
} else {
log("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;
}
}
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);
}
Resource[][] newerResources = new Resource[filesets.length][];
for (int i = 0; i < filesets.length; i++) {
if (!(fileset instanceof ZipFileSet)
|| ((ZipFileSet) fileset).getSrc(getProject()) == null) {
File base = filesets[i].getDir(getProject());
for (int j = 0; j < initialResources[i].length; j++) {
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) {
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
MergingMapper fm = new MergingMapper();
fm.setTo(zfs.getFullpath(getProject()));
myMapper = fm;
} else if (zfs.getPrefix(getProject()) != null
&& !zfs.getPrefix(getProject()).equals("")) {
GlobPatternMapper gm = new GlobPatternMapper();
gm.setFrom("*");
String prefix = zfs.getPrefix(getProject());
if (!prefix.endsWith("/") && !prefix.endsWith("\\")) {
prefix += "/";
}
gm.setTo(prefix + "*");
myMapper = gm;
}
}
Resource[] resources = initialResources[i];
if (doFilesonly) {
resources = selectFileResources(resources);
}
newerResources[i] =
ResourceUtils.selectOutOfDateSources(this,
resources,
myMapper,
getZipScanner());
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(ResourceCollection[] rcs,
File zipFile,
boolean needsUpdate)
throws BuildException {
/*
* Backwards compatibility forces us to repeat the logic of
* getResourcesToAdd(FileSet[], ...) here once again.
*/
Resource[][] initialResources = grabNonFileSetResources(rcs);
if (isEmpty(initialResources)) {
// 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);
}
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++) {
if (initialResources[i][j] instanceof FileResource
&& zipFile.equals(((FileResource)
initialResources[i][j]).getFile())) {
throw new BuildException("A zip file cannot include "
+ "itself", getLocation());
}
}
Resource[] rs = initialResources[i];
if (doFilesonly) {
rs = selectFileResources(rs);
}
newerResources[i] =
ResourceUtils.selectOutOfDateSources(this,
rs,
new IdentityMapper(),
getZipScanner());
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);
}
/**
* 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(FileSet[] filesets) {
Resource[][] result = new Resource[filesets.length][];
for (int i = 0; i < filesets.length; i++) {
boolean skipEmptyNames = true;
if (filesets[i] instanceof ZipFileSet) {
ZipFileSet zfs = (ZipFileSet) filesets[i];
skipEmptyNames = zfs.getPrefix(getProject()).equals("")
&& zfs.getFullpath(getProject()).equals("");
}
DirectoryScanner rs =
filesets[i].getDirectoryScanner(getProject());
if (rs instanceof ZipScanner) {
((ZipScanner) rs).setEncoding(encoding);
}
Vector resources = new Vector();
if (!doFilesonly) {
String[] directories = rs.getIncludedDirectories();
for (int j = 0; j < directories.length; j++) {
if (!"".equals(directories[j]) || !skipEmptyNames) {
resources.addElement(rs.getResource(directories[j]));
}
}
}
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(ResourceCollection[] rcs) {
Resource[][] result = new Resource[rcs.length][];
for (int i = 0; i < rcs.length; i++) {
Iterator iter = rcs[i].iterator();
ArrayList rs = new ArrayList();
int lastDir = 0;
while (iter.hasNext()) {
Resource r = (Resource) iter.next();
if (r.isExists()) {
if (r.isDirectory()) {
rs.add(lastDir++, r);
} else {
rs.add(r);
}
}
}
result[i] = (Resource[]) 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(File dir, ZipOutputStream zOut, String vPath,
int mode)
throws IOException {
zipDir(dir, zOut, vPath, mode, null);
}
/**
* 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.
* @param extra ZipExtraFields to add
* @throws IOException on error
* @since Ant 1.6.3
*/
protected void zipDir(File dir, ZipOutputStream zOut, String vPath,
int mode, ZipExtraField[] extra)
throws IOException {
if (doFilesonly) {
log("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;
}
log("adding directory " + vPath, Project.MSG_VERBOSE);
addedDirs.put(vPath, vPath);
if (!skipWriting) {
ZipEntry ze = new ZipEntry (vPath);
if (dir != null && dir.exists()) {
// ZIPs store time with a granularity of 2 seconds, round up
ze.setTime(dir.lastModified() + (roundUp ? ROUNDUP_MILLIS : 0));
} else {
// ZIPs store time with a granularity of 2 seconds, round up
ze.setTime(System.currentTimeMillis()
+ (roundUp ? ROUNDUP_MILLIS : 0));
}
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);
}
}
/**
* Adds a new entry to the archive, takes care of duplicates as well.
*
* @param in the stream to read data for the entry from.
* @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, ZipOutputStream zOut, String vPath,
long lastModified, File fromArchive, int mode)
throws IOException {
if (entries.contains(vPath)) {
if (duplicate.equals("preserve")) {
log(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
log("duplicate file " + vPath
+ " found, adding.", Project.MSG_VERBOSE);
}
} else {
log("adding entry " + vPath, Project.MSG_VERBOSE);
}
entries.put(vPath, vPath);
if (!skipWriting) {
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;
CRC32 cal = new CRC32();
if (!in.markSupported()) {
// Store data into a byte[]
ByteArrayOutputStream bos = new ByteArrayOutputStream();
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);
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);
zOut.putNextEntry(ze);
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);
}
/**
* 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(File file, ZipOutputStream zOut, String vPath,
int mode)
throws IOException {
if (file.equals(zipFile)) {
throw new BuildException("A zip file cannot include itself",
getLocation());
}
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(File baseDir, String entry,
ZipOutputStream zOut, String prefix,
int dirMode)
throws IOException {
if (!doFilesonly) {
Stack directories = new Stack();
int slashPos = entry.length();
while ((slashPos = entry.lastIndexOf('/', slashPos - 1)) != -1) {
String dir = entry.substring(0, slashPos + 1);
if (addedDirs.get(prefix + dir) != null) {
break;
}
directories.push(dir);
}
while (!directories.isEmpty()) {
String dir = (String) 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;
Enumeration e = filesetsFromGroupfilesets.elements();
while (e.hasMoreElements()) {
ZipFileSet zf = (ZipFileSet) e.nextElement();
resources.removeElement(zf);
}
filesetsFromGroupfilesets.removeAllElements();
}
/**
* 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(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(Resource[] orig) {
if (orig.length == 0) {
return orig;
}
Vector v = new Vector(orig.length);
for (int i = 0; i < orig.length; i++) {
if (!orig[i].isDirectory()) {
v.addElement(orig[i]);
} else {
log("Ignoring directory " + orig[i].getName()
+ " as only files will be added.", Project.MSG_VERBOSE);
}
}
if (v.size() != orig.length) {
Resource[] r = new Resource[v.size()];
v.copyInto(r);
return r;
}
return orig;
}
/**
* Possible behaviors when a duplicate file is added:
* "add", "preserve" or "fail"
*/
public static class Duplicate extends EnumeratedAttribute {
/**
* @see EnumeratedAttribute#getValues()
*/
/** {@inheritDoc} */
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 boolean outOfDate;
private Resource[][] resourcesToAdd;
ArchiveState(boolean state, 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;
}
}
}