All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.zeroturnaround.zip.ZipUtil Maven / Gradle / Ivy

Go to download

The project is intended to have a small, easy and fast library to process ZIP archives. Either create, modify or explode them. On disk or in memory.

There is a newer version: 1.17
Show newest version
/**
 *    Copyright (C) 2012 ZeroTurnaround LLC 
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.zeroturnaround.zip;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
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.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.zip.commons.FileUtils;
import org.zeroturnaround.zip.commons.FilenameUtils;
import org.zeroturnaround.zip.commons.IOUtils;
import org.zeroturnaround.zip.transform.ZipEntryTransformer;
import org.zeroturnaround.zip.transform.ZipEntryTransformerEntry;

/**
 * ZIP file manipulation utilities.
 *
 * @author Rein Raudjärv
 * @author Innokenty Shuvalov
 *
 * @see #containsEntry(File, String)
 * @see #unpackEntry(File, String)
 * @see #unpack(File, File)
 * @see #pack(File, File)
 */
public final class ZipUtil {

  private static final String PATH_SEPARATOR = "/";

  /** Default compression level */
  public static final int DEFAULT_COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION;

  // Use / instead of . to work around an issue with Maven Shade Plugin
  private static final Logger log = LoggerFactory.getLogger("org/zeroturnaround/zip/ZipUtil".replace('/', '.')); // NOSONAR

  private ZipUtil() {
  }

  /* Extracting single entries from ZIP files. */

  /**
   * Checks if the ZIP file contains the given entry.
   *
   * @param zip
   *          ZIP file.
   * @param name
   *          entry name.
   * @return true if the ZIP file contains the given entry.
   */
  public static boolean containsEntry(File zip, String name) {
    ZipFile zf = null;
    try {
      zf = new ZipFile(zip);
      return zf.getEntry(name) != null;
    }
    catch (IOException e) {
      throw ZipExceptionUtil.rethrow(e);
    }
    finally {
      closeQuietly(zf);
    }
  }

  /**
   * Checks if the ZIP file contains any of the given entries.
   *
   * @param zip
   *          ZIP file.
   * @param names
   *          entry names.
   * @return true if the ZIP file contains any of the given
   *         entries.
   */
  public static boolean containsAnyEntry(File zip, String[] names) {
    ZipFile zf = null;
    try {
      zf = new ZipFile(zip);
      for (int i = 0; i < names.length; i++) {
        if (zf.getEntry(names[i]) != null) {
          return true;
        }
      }
      return false;
    }
    catch (IOException e) {
      throw ZipExceptionUtil.rethrow(e);
    }
    finally {
      closeQuietly(zf);
    }
  }

  /**
   * Unpacks a single entry from a ZIP file.
   *
   * @param zip
   *          ZIP file.
   * @param name
   *          entry name.
   * @return contents of the entry or null if it was not found.
   */
  public static byte[] unpackEntry(File zip, String name) {
    ZipFile zf = null;
    try {
      zf = new ZipFile(zip);
      return doUnpackEntry(zf, name);
    }
    catch (IOException e) {
      throw ZipExceptionUtil.rethrow(e);
    }
    finally {
      closeQuietly(zf);
    }
  }

  /**
   * Unpacks a single entry from a ZIP file.
   *
   * @param zf
   *          ZIP file.
   * @param name
   *          entry name.
   * @return contents of the entry or null if it was not found.
   */
  public static byte[] unpackEntry(ZipFile zf, String name) {
    try {
      return doUnpackEntry(zf, name);
    }
    catch (IOException e) {
      throw ZipExceptionUtil.rethrow(e);
    }
  }

  /**
   * Unpacks a single entry from a ZIP file.
   *
   * @param zf
   *          ZIP file.
   * @param name
   *          entry name.
   * @return contents of the entry or null if it was not found.
   */
  private static byte[] doUnpackEntry(ZipFile zf, String name) throws IOException {
    ZipEntry ze = zf.getEntry(name);
    if (ze == null) {
      return null; // entry not found
    }

    InputStream is = zf.getInputStream(ze);
    try {
      return IOUtils.toByteArray(is);
    }
    finally {
      IOUtils.closeQuietly(is);
    }
  }

  /**
   * Unpacks a single entry from a ZIP stream.
   *
   * @param is
   *          ZIP stream.
   * @param name
   *          entry name.
   * @return contents of the entry or null if it was not found.
   */
  public static byte[] unpackEntry(InputStream is, String name) {
    ByteArrayUnpacker action = new ByteArrayUnpacker();
    if (!handle(is, name, action))
      return null; // entry not found
    return action.getBytes();
  }

  /**
   * Copies an entry into a byte array.
   *
   * @author Rein Raudjärv
   */
  private static class ByteArrayUnpacker implements ZipEntryCallback {

    private byte[] bytes;

    public void process(InputStream in, ZipEntry zipEntry) throws IOException {
      bytes = IOUtils.toByteArray(in);
    }

    public byte[] getBytes() {
      return bytes;
    }

  }

  /**
   * Unpacks a single file from a ZIP archive to a file.
   *
   * @param zip
   *          ZIP file.
   * @param name
   *          entry name.
   * @param file
   *          target file to be created or overwritten.
   * @return true if the entry was found and unpacked,
   *         false if the entry was not found.
   */
  public static boolean unpackEntry(File zip, String name, File file) {
    ZipFile zf = null;
    try {
      zf = new ZipFile(zip);
      return doUnpackEntry(zf, name, file);
    }
    catch (IOException e) {
      throw ZipExceptionUtil.rethrow(e);
    }
    finally {
      closeQuietly(zf);
    }
  }

  /**
   * Unpacks a single file from a ZIP archive to a file.
   *
   * @param zf
   *          ZIP file.
   * @param name
   *          entry name.
   * @param file
   *          target file to be created or overwritten.
   * @return true if the entry was found and unpacked,
   *         false if the entry was not found.
   */
  public static boolean unpackEntry(ZipFile zf, String name, File file) {
    try {
      return doUnpackEntry(zf, name, file);
    }
    catch (IOException e) {
      throw ZipExceptionUtil.rethrow(e);
    }
  }

  /**
   * Unpacks a single file from a ZIP archive to a file.
   *
   * @param zf
   *          ZIP file.
   * @param name
   *          entry name.
   * @param file
   *          target file to be created or overwritten.
   * @return true if the entry was found and unpacked,
   *         false if the entry was not found.
   */
  private static boolean doUnpackEntry(ZipFile zf, String name, File file) throws IOException {
    if (log.isTraceEnabled()) {
      log.trace("Extracting '" + zf.getName() + "' entry '" + name + "' into '" + file + "'.");
    }

    ZipEntry ze = zf.getEntry(name);
    if (ze == null) {
      return false; // entry not found
    }

    InputStream in = new BufferedInputStream(zf.getInputStream(ze));
    try {
      FileUtils.copy(in, file);
    }
    finally {
      IOUtils.closeQuietly(in);
    }
    return true;
  }

  /**
   * Unpacks a single file from a ZIP stream to a file.
   *
   * @param is
   *          ZIP stream.
   * @param name
   *          entry name.
   * @param file
   *          target file to be created or overwritten.
   * @return true if the entry was found and unpacked,
   *         false if the entry was not found.
   */
  public static boolean unpackEntry(InputStream is, String name, File file) throws IOException {
    return handle(is, name, new FileUnpacker(file));
  }

  /**
   * Copies an entry into a File.
   *
   * @author Rein Raudjärv
   */
  private static class FileUnpacker implements ZipEntryCallback {

    private final File file;

    public FileUnpacker(File file) {
      this.file = file;
    }

    public void process(InputStream in, ZipEntry zipEntry) throws IOException {
      FileUtils.copy(in, file);
    }

  }

  /* Traversing ZIP files */

  /**
   * Reads the given ZIP file and executes the given action for each entry.
   * 

* For each entry the corresponding input stream is also passed to the action. If you want to stop the loop * then throw a ZipBreakException. * * @param zip * input ZIP file. * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, ZipInfoCallback) */ public static void iterate(File zip, ZipEntryCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); Enumeration en = zf.entries(); while (en.hasMoreElements()) { ZipEntry e = (ZipEntry) en.nextElement(); InputStream is = zf.getInputStream(e); try { action.process(is, e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + "' with action " + action, ze); } catch (ZipBreakException ex) { break; } finally { IOUtils.closeQuietly(is); } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Reads the given ZIP file and executes the given action for each given entry. *

* For each given entry the corresponding input stream is also passed to the action. If you want to stop the loop then throw a ZipBreakException. * * @param zip * input ZIP file. * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, String[], ZipInfoCallback) */ public static void iterate(File zip, String[] entryNames, ZipEntryCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); for (int i = 0; i < entryNames.length; i++) { ZipEntry e = zf.getEntry(entryNames[i]); if (e == null) { continue; } InputStream is = zf.getInputStream(e); try { action.process(is, e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } finally { IOUtils.closeQuietly(is); } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Scans the given ZIP file and executes the given action for each entry. *

* Only the meta-data without the actual data is read. If you want to stop the loop * then throw a ZipBreakException. * * @param zip * input ZIP file. * @param action * action to be called for each entry. * * @see ZipInfoCallback * @see #iterate(File, ZipEntryCallback) */ public static void iterate(File zip, ZipInfoCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); Enumeration en = zf.entries(); while (en.hasMoreElements()) { ZipEntry e = (ZipEntry) en.nextElement(); try { action.process(e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Scans the given ZIP file and executes the given action for each given entry. *

* Only the meta-data without the actual data is read. If you want to stop the loop then throw a ZipBreakException. * * @param zip * input ZIP file. * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * * @see ZipInfoCallback * @see #iterate(File, String[], ZipEntryCallback) */ public static void iterate(File zip, String[] entryNames, ZipInfoCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); for (int i = 0; i < entryNames.length; i++) { ZipEntry e = zf.getEntry(entryNames[i]); if (e == null) { continue; } try { action.process(e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Reads the given ZIP stream and executes the given action for each entry. *

* For each entry the corresponding input stream is also passed to the action. If you want to stop the loop * then throw a ZipBreakException. * * @param is * input ZIP stream (it will not be closed automatically). * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, ZipEntryCallback) */ public static void iterate(InputStream is, ZipEntryCallback action, Charset charset) { try { ZipInputStream in = null; if (charset == null) { in = new ZipInputStream(new BufferedInputStream(is)); } else { in = ZipFileUtil.createZipInputStream(is, charset); } ZipEntry entry; while ((entry = in.getNextEntry()) != null) { try { action.process(in, entry); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + entry.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * See {@link #iterate(InputStream, ZipEntryCallback, Charset)}. This method * is a shorthand for a version where no Charset is specified. * * @param is * input ZIP stream (it will not be closed automatically). * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, ZipEntryCallback) */ public static void iterate(InputStream is, ZipEntryCallback action) { iterate(is, action, null); } /** * Reads the given ZIP stream and executes the given action for each given entry. *

* For each given entry the corresponding input stream is also passed to the action. If you want to stop the loop then throw a ZipBreakException. * * @param is * input ZIP stream (it will not be closed automatically). * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, String[], ZipEntryCallback) */ public static void iterate(InputStream is, String[] entryNames, ZipEntryCallback action, Charset charset) { Set namesSet = new HashSet(); for (int i = 0; i < entryNames.length; i++) { namesSet.add(entryNames[i]); } try { ZipInputStream in = null; if (charset == null) { in = new ZipInputStream(new BufferedInputStream(is)); } else { in = ZipFileUtil.createZipInputStream(is, charset); } ZipEntry entry; while ((entry = in.getNextEntry()) != null) { if (!namesSet.contains(entry.getName())) { // skip the unnecessary entry continue; } try { action.process(in, entry); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + entry.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * See @link{ {@link #iterate(InputStream, ZipEntryCallback, Charset)}. It is a * shorthand where no Charset is specified. * * @param is * input ZIP stream (it will not be closed automatically). * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, String[], ZipEntryCallback) */ public static void iterate(InputStream is, String[] entryNames, ZipEntryCallback action) { iterate(is, entryNames, action, null); } /** * Reads the given ZIP file and executes the given action for a single entry. * * @param zip * input ZIP file. * @param name * entry name. * @param action * action to be called for this entry. * @return true if the entry was found, false if the * entry was not found. * * @see ZipEntryCallback */ public static boolean handle(File zip, String name, ZipEntryCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); ZipEntry ze = zf.getEntry(name); if (ze == null) { return false; // entry not found } InputStream in = new BufferedInputStream(zf.getInputStream(ze)); try { action.process(in, ze); } finally { IOUtils.closeQuietly(in); } return true; } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Reads the given ZIP stream and executes the given action for a single * entry. * * @param is * input ZIP stream (it will not be closed automatically). * @param name * entry name. * @param action * action to be called for this entry. * @return true if the entry was found, false if the * entry was not found. * * @see ZipEntryCallback */ public static boolean handle(InputStream is, String name, ZipEntryCallback action) { SingleZipEntryCallback helper = new SingleZipEntryCallback(name, action); iterate(is, helper); return helper.found(); } /** * ZipEntryCallback which is only applied to single entry. * * @author Rein Raudjärv */ private static class SingleZipEntryCallback implements ZipEntryCallback { private final String name; private final ZipEntryCallback action; private boolean found; public SingleZipEntryCallback(String name, ZipEntryCallback action) { this.name = name; this.action = action; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { if (name.equals(zipEntry.getName())) { found = true; action.process(in, zipEntry); } } public boolean found() { return found; } } /* Extracting whole ZIP files. */ /** * Unpacks a ZIP file to the given directory. *

* The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unpack(File zip, final File outputDir) { unpack(zip, outputDir, IdentityNameMapper.INSTANCE); } /** * Unpacks a ZIP file to the given directory. *

* The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unpack(File zip, File outputDir, NameMapper mapper) { log.debug("Extracting '{}' into '{}'.", zip, outputDir); iterate(zip, new Unpacker(outputDir, mapper)); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. *

* The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unwrap(File zip, final File outputDir) { unwrap(zip, outputDir, IdentityNameMapper.INSTANCE); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. *

* The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unwrap(File zip, File outputDir, NameMapper mapper) { log.debug("Unwraping '{}' into '{}'.", zip, outputDir); iterate(zip, new Unwraper(outputDir, mapper)); } /** * Unpacks a ZIP stream to the given directory. *

* The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unpack(InputStream is, File outputDir) { unpack(is, outputDir, IdentityNameMapper.INSTANCE); } /** * Unpacks a ZIP stream to the given directory. *

* The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unpack(InputStream is, File outputDir, NameMapper mapper) { log.debug("Extracting {} into '{}'.", is, outputDir); iterate(is, new Unpacker(outputDir, mapper)); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. *

* The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unwrap(InputStream is, File outputDir) { unwrap(is, outputDir, IdentityNameMapper.INSTANCE); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. *

* The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unwrap(InputStream is, File outputDir, NameMapper mapper) { log.debug("Unwraping {} into '{}'.", is, outputDir); iterate(is, new Unwraper(outputDir, mapper)); } /** * Unpacks each ZIP entry. * * @author Rein Raudjärv */ private static class Unpacker implements ZipEntryCallback { private final File outputDir; private final NameMapper mapper; public Unpacker(File outputDir, NameMapper mapper) { this.outputDir = outputDir; this.mapper = mapper; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { String name = mapper.map(zipEntry.getName()); if (name != null) { File file = new File(outputDir, name); if (zipEntry.isDirectory()) { FileUtils.forceMkdir(file); } else { FileUtils.forceMkdir(file.getParentFile()); if (log.isDebugEnabled() && file.exists()) { log.debug("Overwriting file '{}'.", zipEntry.getName()); } FileUtils.copy(in, file); } } } } /** * Unwraps entries excluding a single parent dir. If there are multiple roots * ZipException is thrown. * * @author Oleg Shelajev */ private static class Unwraper implements ZipEntryCallback { private final File outputDir; private final NameMapper mapper; private String rootDir; public Unwraper(File outputDir, NameMapper mapper) { this.outputDir = outputDir; this.mapper = mapper; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { String root = getRootName(zipEntry.getName()); if (rootDir == null) { rootDir = root; } else if (!rootDir.equals(root)) { throw new ZipException("Unwrapping with multiple roots is not supported, roots: " + rootDir + ", " + root); } String name = mapper.map(getUnrootedName(root, zipEntry.getName())); if (name != null) { File file = new File(outputDir, name); if (zipEntry.isDirectory()) { FileUtils.forceMkdir(file); } else { FileUtils.forceMkdir(file.getParentFile()); if (log.isDebugEnabled() && file.exists()) { log.debug("Overwriting file '{}'.", zipEntry.getName()); } FileUtils.copy(in, file); } } } private String getUnrootedName(String root, String name) { return name.substring(root.length()); } private String getRootName(final String name) { String newName = name.substring(FilenameUtils.getPrefixLength(name)); int idx = newName.indexOf(PATH_SEPARATOR); if (idx < 0) { throw new ZipException("Entry " + newName + " from the root of the zip is not supported"); } return newName.substring(0, newName.indexOf(PATH_SEPARATOR)); } } /** * Unpacks a ZIP file to its own location. *

* The ZIP file will be first renamed (using a temporary name). After the * extraction it will be deleted. * * @param zip * input ZIP file as well as the target directory. * * @see #unpack(File, File) */ public static void explode(File zip) { try { // Find a new unique name is the same directory File tempFile = FileUtils.getTempFileFor(zip); // Rename the archive FileUtils.moveFile(zip, tempFile); // Unpack it unpack(tempFile, zip); // Delete the archive if (!tempFile.delete()) { throw new IOException("Unable to delete file: " + tempFile); } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /* Compressing single entries to ZIP files. */ /** * Compresses the given file into a ZIP file with single entry. * * @param file file to be compressed. * @return ZIP file created. */ public static byte[] packEntry(File file) { log.trace("Compressing '{}' into a ZIP file with single entry.", file); ByteArrayOutputStream result = new ByteArrayOutputStream(); try { ZipOutputStream out = new ZipOutputStream(result); ZipEntry entry = new ZipEntry(file.getName()); entry.setTime(file.lastModified()); InputStream in = new BufferedInputStream(new FileInputStream(file)); try { ZipEntryUtil.addEntry(entry, in, out); } finally { IOUtils.closeQuietly(in); } out.close(); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } return result.toByteArray(); } /* Compressing ZIP files. */ /** * Compresses the given directory and all its sub-directories into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * Will not include the root directory name in the archive. * * @param rootDir * root directory. * @param zip * ZIP file that will be created or overwritten. */ public static void pack(File rootDir, File zip) { pack(rootDir, zip, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses the given directory and all its sub-directories into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * Will not include the root directory name in the archive. * * @param rootDir * root directory. * @param zip * ZIP file that will be created or overwritten. * @param compressionLevel * compression level */ public static void pack(File rootDir, File zip, int compressionLevel) { pack(rootDir, zip, IdentityNameMapper.INSTANCE, compressionLevel); } /** * Compresses the given directory and all its sub-directories into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * Will not include the root directory name in the archive. * * @param sourceDir * root directory. * @param targetZipFile * ZIP file that will be created or overwritten. */ public static void pack(final File sourceDir, final File targetZipFile, final boolean preserveRoot) { if (preserveRoot) { final String parentName = sourceDir.getName(); pack(sourceDir, targetZipFile, new NameMapper() { public String map(String name) { return parentName + PATH_SEPARATOR + name; } }); } else { pack(sourceDir, targetZipFile); } } /** * Compresses the given file into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * * @param fileToPack * file that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. */ public static void packEntry(File fileToPack, File destZipFile) { packEntry(fileToPack, destZipFile, IdentityNameMapper.INSTANCE); } /** * Compresses the given file into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * * @param fileToPack * file that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param fileName * the name for the file inside the archive */ public static void packEntry(File fileToPack, File destZipFile, final String fileName) { packEntry(fileToPack, destZipFile, new NameMapper() { public String map(String name) { return fileName; } }); } /** * Compresses the given file into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * * @param fileToPack * file that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. */ public static void packEntry(File fileToPack, File destZipFile, NameMapper mapper) { packEntries(new File[] { fileToPack }, destZipFile, mapper); } /** * Compresses the given files into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * * @param filesToPack * files that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. */ public static void packEntries(File[] filesToPack, File destZipFile) { packEntries(filesToPack, destZipFile, IdentityNameMapper.INSTANCE); } /** * Compresses the given files into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * * @param filesToPack * files that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. */ public static void packEntries(File[] filesToPack, File destZipFile, NameMapper mapper) { log.debug("Compressing '{}' into '{}'.", filesToPack, destZipFile); ZipOutputStream out = null; FileOutputStream fos = null; try { fos = new FileOutputStream(destZipFile); out = new ZipOutputStream(new BufferedOutputStream(fos)); for (int i = 0; i < filesToPack.length; i++) { File fileToPack = filesToPack[i]; ZipEntry zipEntry = new ZipEntry(mapper.map(fileToPack.getName())); zipEntry.setSize(fileToPack.length()); zipEntry.setTime(fileToPack.lastModified()); out.putNextEntry(zipEntry); FileUtils.copy(fileToPack, out); out.closeEntry(); } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); IOUtils.closeQuietly(fos); } } /** * Compresses the given directory and all its sub-directories into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * * @param sourceDir * root directory. * @param targetZip * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. */ public static void pack(File sourceDir, File targetZip, NameMapper mapper) { pack(sourceDir, targetZip, mapper, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses the given directory and all its sub-directories into a ZIP file. *

* The ZIP file must not be a directory and its parent directory must exist. * * @param sourceDir * root directory. * @param targetZip * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. * @param compressionLevel * compression level */ public static void pack(File sourceDir, File targetZip, NameMapper mapper, int compressionLevel) { log.debug("Compressing '{}' into '{}'.", sourceDir, targetZip); if (!sourceDir.exists()) { throw new ZipException("Given file '" + sourceDir + "' doesn't exist!"); } ZipOutputStream out = null; try { out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(targetZip))); out.setLevel(compressionLevel); pack(sourceDir, out, mapper, "", true); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); } } /** * Compresses the given directory and all its sub-directories into a ZIP file. * * @param dir * root directory. * @param out * ZIP output stream. * @param mapper * call-back for renaming the entries. * @param pathPrefix * prefix to be used for the entries. * @param mustHaveChildren * if true, but directory to pack doesn't have any files, throw an exception. */ private static void pack(File dir, ZipOutputStream out, NameMapper mapper, String pathPrefix, boolean mustHaveChildren) throws IOException { String[] filenames = dir.list(); if (filenames == null) { if (!dir.exists()) { throw new ZipException("Given file '" + dir + "' doesn't exist!"); } throw new IOException("Given file is not a directory '" + dir + "'"); } if (mustHaveChildren && filenames.length == 0) { throw new ZipException("Given directory '" + dir + "' doesn't contain any files!"); } for (int i = 0; i < filenames.length; i++) { String filename = filenames[i]; File file = new File(dir, filename); boolean isDir = file.isDirectory(); String path = pathPrefix + file.getName(); // NOSONAR if (isDir) { path += PATH_SEPARATOR; // NOSONAR } // Create a ZIP entry String name = mapper.map(path); if (name != null) { ZipEntry zipEntry = new ZipEntry(name); if (!isDir) { zipEntry.setSize(file.length()); zipEntry.setTime(file.lastModified()); } out.putNextEntry(zipEntry); // Copy the file content if (!isDir) { FileUtils.copy(file, out); } out.closeEntry(); } // Traverse the directory if (isDir) { pack(file, out, mapper, path, false); } } } /** * Repacks a provided ZIP file into a new ZIP with a given compression level. *

* * @param srcZip * source ZIP file. * @param dstZip * destination ZIP file. * @param compressionLevel * compression level. */ public static void repack(File srcZip, File dstZip, int compressionLevel) { log.debug("Repacking '{}' into '{}'.", srcZip, dstZip); RepackZipEntryCallback callback = new RepackZipEntryCallback(dstZip, compressionLevel); try { iterate(srcZip, callback); } finally { callback.closeStream(); } } /** * Repacks a provided ZIP input stream into a ZIP file with a given compression level. *

* * @param is * ZIP input stream. * @param dstZip * destination ZIP file. * @param compressionLevel * compression level. */ public static void repack(InputStream is, File dstZip, int compressionLevel) { log.debug("Repacking from input stream into '{}'.", dstZip); RepackZipEntryCallback callback = new RepackZipEntryCallback(dstZip, compressionLevel); try { iterate(is, callback); } finally { callback.closeStream(); } } /** * Repacks a provided ZIP file and replaces old file with the new one. *

* * @param zip * source ZIP file to be repacked and replaced. * @param compressionLevel * compression level. */ public static void repack(File zip, int compressionLevel) { try { File tmpZip = FileUtils.getTempFileFor(zip); repack(zip, tmpZip, compressionLevel); // Delete original zip if (!zip.delete()) { throw new IOException("Unable to delete the file: " + zip); } // Rename the archive FileUtils.moveFile(tmpZip, zip); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * RepackZipEntryCallback used in repacking methods. * * @author Pavel Grigorenko */ private static final class RepackZipEntryCallback implements ZipEntryCallback { private ZipOutputStream out; private RepackZipEntryCallback(File dstZip, int compressionLevel) { try { this.out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(dstZip))); this.out.setLevel(compressionLevel); } catch (IOException e) { ZipExceptionUtil.rethrow(e); } } public void process(InputStream in, ZipEntry zipEntry) throws IOException { ZipEntryUtil.copyEntry(zipEntry, in, out); } private void closeStream() { IOUtils.closeQuietly(out); } } /** * Compresses a given directory in its own location. *

* A ZIP file will be first created with a temporary name. After the * compressing the directory will be deleted and the ZIP file will be renamed * as the original directory. * * @param dir * input directory as well as the target ZIP file. * * @see #pack(File, File) */ public static void unexplode(File dir) { unexplode(dir, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses a given directory in its own location. *

* A ZIP file will be first created with a temporary name. After the * compressing the directory will be deleted and the ZIP file will be renamed * as the original directory. * * @param dir * input directory as well as the target ZIP file. * @param compressionLevel * compression level * * @see #pack(File, File) */ public static void unexplode(File dir, int compressionLevel) { try { // Find a new unique name is the same directory File zip = FileUtils.getTempFileFor(dir); // Pack it pack(dir, zip, compressionLevel); // Delete the directory FileUtils.deleteDirectory(dir); // Rename the archive FileUtils.moveFile(zip, dir); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Compresses the given entries into a new ZIP file. * * @param entries * ZIP entries added. * @param zip * new ZIP file created. */ public static void pack(ZipEntrySource[] entries, File zip) { log.debug("Creating '{}' from {}.", zip, Arrays.asList(entries)); ZipOutputStream out = null; try { out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zip))); for (int i = 0; i < entries.length; i++) { addEntry(entries[i], out); } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); } } /** * Copies an existing ZIP file and appends it with one new entry. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param file * new entry to be added. * @param destZip * new ZIP file created. */ public static void addEntry(File zip, String path, File file, File destZip) { addEntry(zip, new FileSource(path, file), destZip); } /** * Changes a zip file, adds one new entry in-place. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param file * new entry to be added. */ public static void addEntry(final File zip, final String path, final File file) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntry(zip, path, file, tmpFile); return true; } }); } /** * Copies an existing ZIP file and appends it with one new entry. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param bytes * new entry bytes (or null if directory). * @param destZip * new ZIP file created. */ public static void addEntry(File zip, String path, byte[] bytes, File destZip) { addEntry(zip, new ByteSource(path, bytes), destZip); } /** * Changes a zip file, adds one new entry in-place. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param bytes * new entry bytes (or null if directory). */ public static void addEntry(final File zip, final String path, final byte[] bytes) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntry(zip, path, bytes, tmpFile); return true; } }); } /** * Copies an existing ZIP file and appends it with one new entry. * * @param zip * an existing ZIP file (only read). * @param entry * new ZIP entry appended. * @param destZip * new ZIP file created. */ public static void addEntry(File zip, ZipEntrySource entry, File destZip) { addEntries(zip, new ZipEntrySource[] { entry }, destZip); } /** * Changes a zip file, adds one new entry in-place. * * @param zip * an existing ZIP file (only read). * @param entry * new ZIP entry appended. */ public static void addEntry(final File zip, final ZipEntrySource entry) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntry(zip, entry, tmpFile); return true; } }); } /** * Copies an existing ZIP file and appends it with new entries. * * @param zip * an existing ZIP file (only read). * @param entries * new ZIP entries appended. * @param destZip * new ZIP file created. */ public static void addEntries(File zip, ZipEntrySource[] entries, File destZip) { if (log.isDebugEnabled()) { log.debug("Copying '" + zip + "' to '" + destZip + "' and adding " + Arrays.asList(entries) + "."); } ZipOutputStream out = null; try { out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); copyEntries(zip, out); for (int i = 0; i < entries.length; i++) { addEntry(entries[i], out); } } catch (IOException e) { ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); } } /** * Changes a zip file it with with new entries. in-place. * * @param zip * an existing ZIP file (only read). * @param entries * new ZIP entries appended. */ public static void addEntries(final File zip, final ZipEntrySource[] entries) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntries(zip, entries, tmpFile); return true; } }); } /** * Copies an existing ZIP file and removes entry with a given path. * * @param zip * an existing ZIP file (only read) * @param path * path of the entry to remove * @param destZip * new ZIP file created. * @since 1.7 */ public static void removeEntry(File zip, String path, File destZip) { removeEntries(zip, new String[] { path }, destZip); } /** * Changes an existing ZIP file: removes entry with a given path. * * @param zip * an existing ZIP file * @param path * path of the entry to remove * @since 1.7 */ public static void removeEntry(final File zip, final String path) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { removeEntry(zip, path, tmpFile); return true; } }); } /** * Copies an existing ZIP file and removes entries with given paths. * * @param zip * an existing ZIP file (only read) * @param paths * paths of the entries to remove * @param destZip * new ZIP file created. * @since 1.7 */ public static void removeEntries(File zip, String[] paths, File destZip) { if (log.isDebugEnabled()) { log.debug("Copying '" + zip + "' to '" + destZip + "' and removing paths " + Arrays.asList(paths) + "."); } ZipOutputStream out = null; try { out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); copyEntries(zip, out, new HashSet(Arrays.asList(paths))); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); } } /** * Changes an existing ZIP file: removes entries with given paths. * * @param zip * an existing ZIP file * @param paths * paths of the entries to remove * @since 1.7 */ public static void removeEntries(final File zip, final String[] paths) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { removeEntries(zip, paths, tmpFile); return true; } }); } /** * Copies all entries from one ZIP file to another. * * @param zip * source ZIP file. * @param out * target ZIP stream. */ private static void copyEntries(File zip, final ZipOutputStream out) { // this one doesn't call copyEntries with ignoredEntries, because that has poorer performance final Set names = new HashSet(); iterate(zip, new ZipEntryCallback() { public void process(InputStream in, ZipEntry zipEntry) throws IOException { String entryName = zipEntry.getName(); if (names.add(entryName)) { ZipEntryUtil.copyEntry(zipEntry, in, out); } else if (log.isDebugEnabled()) { log.debug("Duplicate entry: {}", entryName); } } }); } /** * Copies all entries from one ZIP file to another, ignoring entries with path in ignoredEntries * * @param zip * source ZIP file. * @param out * target ZIP stream. * @param ignoredEntries * paths of entries not to copy */ private static void copyEntries(File zip, final ZipOutputStream out, final Set ignoredEntries) { final Set names = new HashSet(); final Set dirNames = filterDirEntries(zip, ignoredEntries); iterate(zip, new ZipEntryCallback() { public void process(InputStream in, ZipEntry zipEntry) throws IOException { String entryName = zipEntry.getName(); if (ignoredEntries.contains(entryName)) { return; } Iterator iter = dirNames.iterator(); while (iter.hasNext()) { String dirName = (String) iter.next(); if (entryName.startsWith(dirName)) { return; } } if (names.add(entryName)) { ZipEntryUtil.copyEntry(zipEntry, in, out); } else if (log.isDebugEnabled()) { log.debug("Duplicate entry: {}", entryName); } } }); } /** * * @param zip * zip file to traverse * @param names * names of entries to filter dirs from * @return Set names of entries that are dirs. * */ static Set filterDirEntries(File zip, Collection names) { Set dirs = new HashSet(); if (zip == null) { return dirs; } ZipFile zf = null; try { zf = new ZipFile(zip); Iterator iterator = names.iterator(); while (iterator.hasNext()) { String entryName = (String) iterator.next(); ZipEntry entry = zf.getEntry(entryName); if (entry.isDirectory()) { dirs.add(entry.getName()); } else if (zf.getInputStream(entry) == null) { // no input stream means that this is a dir. dirs.add(entry.getName() + PATH_SEPARATOR); } } } catch (IOException e) { ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } return dirs; } /** * Copies an existing ZIP file and replaces a given entry in it. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param file * new entry. * @param destZip * new ZIP file created. * @return true if the entry was replaced. */ public static boolean replaceEntry(File zip, String path, File file, File destZip) { return replaceEntry(zip, new FileSource(path, file), destZip); } /** * Changes an existing ZIP file: replaces a given entry in it. * * @param zip * an existing ZIP file. * @param path * new ZIP entry path. * @param file * new entry. * @return true if the entry was replaced. */ public static boolean replaceEntry(final File zip, final String path, final File file) { return operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { return replaceEntry(zip, new FileSource(path, file), tmpFile); } }); } /** * Copies an existing ZIP file and replaces a given entry in it. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param bytes * new entry bytes (or null if directory). * @param destZip * new ZIP file created. * @return true if the entry was replaced. */ public static boolean replaceEntry(File zip, String path, byte[] bytes, File destZip) { return replaceEntry(zip, new ByteSource(path, bytes), destZip); } /** * Changes an existing ZIP file: replaces a given entry in it. * * @param zip * an existing ZIP file. * @param path * new ZIP entry path. * @param bytes * new entry bytes (or null if directory). * @return true if the entry was replaced. */ public static boolean replaceEntry(final File zip, final String path, final byte[] bytes) { return operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { return replaceEntry(zip, new ByteSource(path, bytes), tmpFile); } }); } /** * Copies an existing ZIP file and replaces a given entry in it. * * @param zip * an existing ZIP file (only read). * @param entry * new ZIP entry. * @param destZip * new ZIP file created. * @return true if the entry was replaced. */ public static boolean replaceEntry(File zip, ZipEntrySource entry, File destZip) { return replaceEntries(zip, new ZipEntrySource[] { entry }, destZip); } /** * Changes an existing ZIP file: replaces a given entry in it. * * @param zip * an existing ZIP file. * @param entry * new ZIP entry. * @return true if the entry was replaced. */ public static boolean replaceEntry(final File zip, final ZipEntrySource entry) { return operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { return replaceEntry(zip, entry, tmpFile); } }); } /** * Copies an existing ZIP file and replaces the given entries in it. * * @param zip * an existing ZIP file (only read). * @param entries * new ZIP entries to be replaced with. * @param destZip * new ZIP file created. * @return true if at least one entry was replaced. */ public static boolean replaceEntries(File zip, ZipEntrySource[] entries, File destZip) { if (log.isDebugEnabled()) { log.debug("Copying '" + zip + "' to '" + destZip + "' and replacing entries " + Arrays.asList(entries) + "."); } final Map entryByPath = byPath(entries); final int entryCount = entryByPath.size(); try { final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); try { final Set names = new HashSet(); iterate(zip, new ZipEntryCallback() { public void process(InputStream in, ZipEntry zipEntry) throws IOException { if (names.add(zipEntry.getName())) { ZipEntrySource entry = (ZipEntrySource) entryByPath.remove(zipEntry.getName()); if (entry != null) { addEntry(entry, out); } else { ZipEntryUtil.copyEntry(zipEntry, in, out); } } else if (log.isDebugEnabled()) { log.debug("Duplicate entry: {}", zipEntry.getName()); } } }); } finally { IOUtils.closeQuietly(out); } } catch (IOException e) { ZipExceptionUtil.rethrow(e); } return entryByPath.size() < entryCount; } /** * Changes an existing ZIP file: replaces a given entry in it. * * @param zip * an existing ZIP file. * @param entries * new ZIP entries to be replaced with. * @return true if at least one entry was replaced. */ public static boolean replaceEntries(final File zip, final ZipEntrySource[] entries) { return operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { return replaceEntries(zip, entries, tmpFile); } }); } /** * Copies an existing ZIP file and adds/replaces the given entries in it. * * @param zip * an existing ZIP file (only read). * @param entries * ZIP entries to be replaced or added. * @param destZip * new ZIP file created. */ public static void addOrReplaceEntries(File zip, ZipEntrySource[] entries, File destZip) { if (log.isDebugEnabled()) { log.debug("Copying '" + zip + "' to '" + destZip + "' and adding/replacing entries " + Arrays.asList(entries) + "."); } final Map entryByPath = byPath(entries); try { final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); try { // Copy and replace entries final Set names = new HashSet(); iterate(zip, new ZipEntryCallback() { public void process(InputStream in, ZipEntry zipEntry) throws IOException { if (names.add(zipEntry.getName())) { ZipEntrySource entry = (ZipEntrySource) entryByPath.remove(zipEntry.getName()); if (entry != null) { addEntry(entry, out); } else { ZipEntryUtil.copyEntry(zipEntry, in, out); } } else if (log.isDebugEnabled()) { log.debug("Duplicate entry: {}", zipEntry.getName()); } } }); // Add new entries for (Iterator it = entryByPath.values().iterator(); it.hasNext();) { addEntry((ZipEntrySource) it.next(), out); } } finally { IOUtils.closeQuietly(out); } } catch (IOException e) { ZipExceptionUtil.rethrow(e); } } /** * Changes a ZIP file: adds/replaces the given entries in it. * * @param zip * an existing ZIP file (only read). * @param entries * ZIP entries to be replaced or added. */ public static void addOrReplaceEntries(final File zip, final ZipEntrySource[] entries) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addOrReplaceEntries(zip, entries, tmpFile); return true; } }); } /** * @return given entries indexed by path. */ static Map byPath(ZipEntrySource[] entries) { Map result = new HashMap(); for (int i = 0; i < entries.length; i++) { ZipEntrySource source = entries[i]; result.put(source.getPath(), source); } return result; } /** * @return given entries indexed by path. */ static Map byPath(Collection entries) { Map result = new HashMap(); Iterator iter = entries.iterator(); while (iter.hasNext()) { ZipEntrySource source = (ZipEntrySource) iter.next(); result.put(source.getPath(), source); } return result; } /** * Copies an existing ZIP file and transforms a given entry in it. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param transformer * transformer for the given ZIP entry. * @param destZip * new ZIP file created. * @return true if the entry was replaced. */ public static boolean transformEntry(File zip, String path, ZipEntryTransformer transformer, File destZip) { return transformEntry(zip, new ZipEntryTransformerEntry(path, transformer), destZip); } /** * Changes an existing ZIP file: transforms a given entry in it. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param transformer * transformer for the given ZIP entry. * @return true if the entry was replaced. */ public static boolean transformEntry(final File zip, final String path, final ZipEntryTransformer transformer) { return operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { return transformEntry(zip, path, transformer, tmpFile); } }); } /** * Copies an existing ZIP file and transforms a given entry in it. * * @param zip * an existing ZIP file (only read). * @param entry * transformer for a ZIP entry. * @param destZip * new ZIP file created. * @return true if the entry was replaced. */ public static boolean transformEntry(File zip, ZipEntryTransformerEntry entry, File destZip) { return transformEntries(zip, new ZipEntryTransformerEntry[] { entry }, destZip); } /** * Changes an existing ZIP file: transforms a given entry in it. * * @param zip * an existing ZIP file (only read). * @param entry * transformer for a ZIP entry. * @return true if the entry was replaced. */ public static boolean transformEntry(final File zip, final ZipEntryTransformerEntry entry) { return operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { return transformEntry(zip, entry, tmpFile); } }); } /** * Copies an existing ZIP file and transforms the given entries in it. * * @param zip * an existing ZIP file (only read). * @param entries * ZIP entry transformers. * @param destZip * new ZIP file created. * @return true if at least one entry was replaced. */ public static boolean transformEntries(File zip, ZipEntryTransformerEntry[] entries, File destZip) { if (log.isDebugEnabled()) log.debug("Copying '" + zip + "' to '" + destZip + "' and transforming entries " + Arrays.asList(entries) + "."); try { ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); try { TransformerZipEntryCallback action = new TransformerZipEntryCallback(entries, out); iterate(zip, action); return action.found(); } finally { IOUtils.closeQuietly(out); } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Changes an existing ZIP file: transforms a given entries in it. * * @param zip * an existing ZIP file (only read). * @param entries * ZIP entry transformers. * @return true if the entry was replaced. */ public static boolean transformEntries(final File zip, final ZipEntryTransformerEntry[] entries) { return operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { return transformEntries(zip, entries, tmpFile); } }); } /** * Copies an existing ZIP file and transforms a given entry in it. * * @param is * a ZIP input stream. * @param path * new ZIP entry path. * @param transformer * transformer for the given ZIP entry. * @param os * a ZIP output stream. * @return true if the entry was replaced. */ public static boolean transformEntry(InputStream is, String path, ZipEntryTransformer transformer, OutputStream os) { return transformEntry(is, new ZipEntryTransformerEntry(path, transformer), os); } /** * Copies an existing ZIP file and transforms a given entry in it. * * @param is * a ZIP input stream. * @param entry * transformer for a ZIP entry. * @param os * a ZIP output stream. * @return true if the entry was replaced. */ public static boolean transformEntry(InputStream is, ZipEntryTransformerEntry entry, OutputStream os) { return transformEntries(is, new ZipEntryTransformerEntry[] { entry }, os); } /** * Copies an existing ZIP file and transforms the given entries in it. * * @param is * a ZIP input stream. * @param entries * ZIP entry transformers. * @param os * a ZIP output stream. * @return true if at least one entry was replaced. */ public static boolean transformEntries(InputStream is, ZipEntryTransformerEntry[] entries, OutputStream os) { if (log.isDebugEnabled()) log.debug("Copying '" + is + "' to '" + os + "' and transforming entries " + Arrays.asList(entries) + "."); try { ZipOutputStream out = new ZipOutputStream(os); TransformerZipEntryCallback action = new TransformerZipEntryCallback(entries, out); iterate(is, action); // Finishes writing the contents of the ZIP output stream without closing // the underlying stream. out.finish(); return action.found(); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } private static class TransformerZipEntryCallback implements ZipEntryCallback { private final Map entryByPath; private final int entryCount; private final ZipOutputStream out; private final Set names = new HashSet(); public TransformerZipEntryCallback(ZipEntryTransformerEntry[] entries, ZipOutputStream out) { entryByPath = byPath(entries); entryCount = entryByPath.size(); this.out = out; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { if (names.add(zipEntry.getName())) { ZipEntryTransformer entry = (ZipEntryTransformer) entryByPath.remove(zipEntry.getName()); if (entry != null) { entry.transform(in, zipEntry, out); } else { ZipEntryUtil.copyEntry(zipEntry, in, out); } } else if (log.isDebugEnabled()) { log.debug("Duplicate entry: {}", zipEntry.getName()); } } /** * @return true if at least one entry was replaced. */ public boolean found() { return entryByPath.size() < entryCount; } } /** * @return transformers by path. */ static Map byPath(ZipEntryTransformerEntry[] entries) { Map result = new HashMap(); for (int i = 0; i < entries.length; i++) { ZipEntryTransformerEntry entry = entries[i]; result.put(entry.getPath(), entry.getTransformer()); } return result; } /** * Adds a given ZIP entry to a ZIP file. * * @param entry * new ZIP entry. * @param out * target ZIP stream. */ private static void addEntry(ZipEntrySource entry, ZipOutputStream out) throws IOException { out.putNextEntry(entry.getEntry()); InputStream in = entry.getInputStream(); if (in != null) { try { IOUtils.copy(in, out); } finally { IOUtils.closeQuietly(in); } } out.closeEntry(); } /* Comparing two ZIP files. */ /** * Compares two ZIP files and returns true if they contain same * entries. *

* First the two files are compared byte-by-byte. If a difference is found the * corresponding entries of both ZIP files are compared. Thus if same contents * is packed differently the two archives may still be the same. *

*

* Two archives are considered the same if *

    *
  1. they contain same number of entries,
  2. *
  3. for each entry in the first archive there exists an entry with the same * in the second archive
  4. *
  5. for each entry in the first archive and the entry with the same name in * the second archive *
      *
    1. both are either directories or files,
    2. *
    3. both have the same size,
    4. *
    5. both have the same CRC,
    6. *
    7. both have the same contents (compared byte-by-byte).
    8. *
    *
  6. *
* * @param f1 * first ZIP file. * @param f2 * second ZIP file. * @return true if the two ZIP files contain same entries, * false if a difference was found or an error occurred * during the comparison. */ public static boolean archiveEquals(File f1, File f2) { try { // Check the files byte-by-byte if (FileUtils.contentEquals(f1, f2)) { return true; } log.debug("Comparing archives '{}' and '{}'...", f1, f2); long start = System.currentTimeMillis(); boolean result = archiveEqualsInternal(f1, f2); long time = System.currentTimeMillis() - start; if (time > 0) { log.debug("Archives compared in " + time + " ms."); } return result; } catch (Exception e) { log.debug("Could not compare '" + f1 + "' and '" + f2 + "':", e); return false; } } private static boolean archiveEqualsInternal(File f1, File f2) throws IOException { ZipFile zf1 = null; ZipFile zf2 = null; try { zf1 = new ZipFile(f1); zf2 = new ZipFile(f2); // Check the number of entries if (zf1.size() != zf2.size()) { log.debug("Number of entries changed (" + zf1.size() + " vs " + zf2.size() + ")."); return false; } /* * As there are same number of entries in both archives we can traverse * all entries of one of the archives and get the corresponding entries * from the other archive. * * If a corresponding entry is missing from the second archive the * archives are different and we finish the comparison. * * We guarantee that no entry of the second archive is skipped as there * are same number of unique entries in both archives. */ Enumeration en = zf1.entries(); while (en.hasMoreElements()) { ZipEntry e1 = (ZipEntry) en.nextElement(); String path = e1.getName(); ZipEntry e2 = zf2.getEntry(path); // Check meta data if (!metaDataEquals(path, e1, e2)) { return false; } // Check the content InputStream is1 = null; InputStream is2 = null; try { is1 = zf1.getInputStream(e1); is2 = zf2.getInputStream(e2); if (!IOUtils.contentEquals(is1, is2)) { log.debug("Entry '{}' content changed.", path); return false; } } finally { IOUtils.closeQuietly(is1); IOUtils.closeQuietly(is2); } } } finally { closeQuietly(zf1); closeQuietly(zf2); } log.debug("Archives are the same."); return true; } /** * Compares meta-data of two ZIP entries. *

* Two entries are considered the same if *

    *
  1. both entries exist,
  2. *
  3. both entries are either directories or files,
  4. *
  5. both entries have the same size,
  6. *
  7. both entries have the same CRC.
  8. *
* * @param path * name of the entries. * @param e1 * first entry (required). * @param e2 * second entry (may be null). * @return true if no difference was found. */ private static boolean metaDataEquals(String path, ZipEntry e1, ZipEntry e2) throws IOException { // Check if the same entry exists in the second archive if (e2 == null) { log.debug("Entry '{}' removed.", path); return false; } // Check the directory flag if (e1.isDirectory()) { if (e2.isDirectory()) { return true; // Let's skip the directory as there is nothing to compare } else { log.debug("Entry '{}' not a directory any more.", path); return false; } } else if (e2.isDirectory()) { log.debug("Entry '{}' now a directory.", path); return false; } // Check the size long size1 = e1.getSize(); long size2 = e2.getSize(); if (size1 != -1 && size2 != -1 && size1 != size2) { log.debug("Entry '" + path + "' size changed (" + size1 + " vs " + size2 + ")."); return false; } // Check the CRC long crc1 = e1.getCrc(); long crc2 = e2.getCrc(); if (crc1 != -1 && crc2 != -1 && crc1 != crc2) { log.debug("Entry '" + path + "' CRC changed (" + crc1 + " vs " + crc2 + ")."); return false; } // Check the time (ignored, logging only) if (log.isTraceEnabled()) { long time1 = e1.getTime(); long time2 = e2.getTime(); if (time1 != -1 && time2 != -1 && time1 != time2) { log.trace("Entry '" + path + "' time changed (" + new Date(time1) + " vs " + new Date(time2) + ")."); } } return true; } /** * Compares same entry in two ZIP files (byte-by-byte). * * @param f1 * first ZIP file. * @param f2 * second ZIP file. * @param path * name of the entry. * @return true if the contents of the entry was same in both ZIP * files. */ public static boolean entryEquals(File f1, File f2, String path) { return entryEquals(f1, f2, path, path); } /** * Compares two ZIP entries (byte-by-byte). . * * @param f1 * first ZIP file. * @param f2 * second ZIP file. * @param path1 * name of the first entry. * @param path2 * name of the second entry. * @return true if the contents of the entries were same. */ public static boolean entryEquals(File f1, File f2, String path1, String path2) { ZipFile zf1 = null; ZipFile zf2 = null; try { zf1 = new ZipFile(f1); zf2 = new ZipFile(f2); return doEntryEquals(zf1, zf2, path1, path2); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf1); closeQuietly(zf2); } } /** * Compares two ZIP entries (byte-by-byte). . * * @param zf1 * first ZIP file. * @param zf2 * second ZIP file. * @param path1 * name of the first entry. * @param path2 * name of the second entry. * @return true if the contents of the entries were same. */ public static boolean entryEquals(ZipFile zf1, ZipFile zf2, String path1, String path2) { try { return doEntryEquals(zf1, zf2, path1, path2); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Compares two ZIP entries (byte-by-byte). . * * @param zf1 * first ZIP file. * @param zf2 * second ZIP file. * @param path1 * name of the first entry. * @param path2 * name of the second entry. * @return true if the contents of the entries were same. */ private static boolean doEntryEquals(ZipFile zf1, ZipFile zf2, String path1, String path2) throws IOException { InputStream is1 = null; InputStream is2 = null; try { ZipEntry e1 = zf1.getEntry(path1); ZipEntry e2 = zf2.getEntry(path2); if (e1 == null && e2 == null) { return true; } if (e1 == null || e2 == null) { return false; } is1 = zf1.getInputStream(e1); is2 = zf2.getInputStream(e2); if (is1 == null && is2 == null) { return true; } if (is1 == null || is2 == null) { return false; } return IOUtils.contentEquals(is1, is2); } finally { IOUtils.closeQuietly(is1); IOUtils.closeQuietly(is2); } } /** * Closes the ZIP file while ignoring any errors. * * @param zf * ZIP file to be closed. */ public static void closeQuietly(ZipFile zf) { try { if (zf != null) { zf.close(); } } catch (IOException e) { } } /** * Simple helper to make inplace operation easier * * @author shelajev */ private abstract static class InPlaceAction { /** * @return true if something has been changed during the action. */ abstract boolean act(File tmpFile); } /** * * This method provides a general infrastructure for in-place operations. * It creates temp file as a destination, then invokes the action on source and destination. * Then it copies the result back into src file. * * @param src - source zip file we want to modify * @param action - action which actually modifies the archives * * @return result of the action */ private static boolean operateInPlace(File src, InPlaceAction action) { File tmp = null; try { tmp = File.createTempFile("zt-zip-tmp", ".zip"); boolean result = action.act(tmp); if (result) { // else nothing changes FileUtils.forceDelete(src); FileUtils.moveFile(tmp, src); } return result; } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { FileUtils.deleteQuietly(tmp); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy