Package org.zeroturnaround.zip

Source Code of org.zeroturnaround.zip.ZipUtil$ByteArrayUnpacker

/**
*    Copyright (C) 2012 ZeroTurnaround LLC <support@zeroturnaround.com>
*
*    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.List;
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 <code>true</code> 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 <code>true</code> 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 <code>null</code> 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 <code>null</code> 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 <code>null</code> 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 <code>null</code> 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 <code>true</code> if the entry was found and unpacked,
   *         <code>false</code> 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 <code>true</code> if the entry was found and unpacked,
   *         <code>false</code> 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 <code>true</code> if the entry was found and unpacked,
   *         <code>false</code> 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
    }

    if(ze.isDirectory() || zf.getInputStream(ze) == null) {
      if(file.isDirectory()) {
        return true;
      }
      if(file.exists()) {
        FileUtils.forceDelete(file);
      }
      return file.mkdirs();
    }

    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 <code>true</code> if the entry was found and unpacked,
   *         <code>false</code> if the entry was not found.
   * @throws java.io.IOException if file is not found or writing to it fails
   */
  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.
   * <p>
   * 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<? extends ZipEntry> 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.
   * <p>
   * 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.
   * <p>
   * 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<? extends ZipEntry> 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.
   * <p>
   * 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.
   * <p>
   * 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.
   * @param charset
   *          charset to process entries in
   *
   * @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.
   * <p>
   * 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.
   * @param charset
   *          charset to process entries in
   *
   * @see ZipEntryCallback
   * @see #iterate(File, String[], ZipEntryCallback)
   */
  public static void iterate(InputStream is, String[] entryNames, ZipEntryCallback action, Charset charset) {
    Set<String> namesSet = new HashSet<String>();
    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 <code>true</code> if the entry was found, <code>false</code> 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 <code>true</code> if the entry was found, <code>false</code> 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.
   * <p>
   * 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.
   * <p>
   * 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.
   * <p>
   * 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.
   * <p>
   * 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("Unwrapping '{}' into '{}'.", zip, outputDir);
    iterate(zip, new Unwraper(outputDir, mapper));
  }

  /**
   * Unpacks a ZIP stream to the given directory.
   * <p>
   * 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.
   * <p>
   * 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.
   * <p>
   * 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.
   * <p>
   * 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("Unwrapping {} 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);
        }
       
        ZTFilePermissions permissions = ZipEntryUtil.getZTFilePermissions(zipEntry);
        if (permissions != null) {
          ZTFilePermissionsUtil.getDefaultStategy().setPermissions(file, permissions);
        }
      }
    }
  }

  /**
   * 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.
   * <p>
   * 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 = ZipEntryUtil.fromFile(file.getName(), file);
      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.
   * <p>
   * 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.
   * <p>
   * 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.
   * <p>
   * 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.
   * @param preserveRoot
   *          true if the resulted archive should have the top directory entry
   */
  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.
   * <p>
   * 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.
     * <p>
     * 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.
   * <p>
   * 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.
   * <p>
   * 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.
   * <p>
   * 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 =  ZipEntryUtil.fromFile(mapper.map(fileToPack.getName()), fileToPack);
        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.
   * <p>
   * 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.
   * <p>
   * 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 = ZipEntryUtil.fromFile(name, file);

        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.
   * <p>
   *
   * @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.
   * <p>
   *
   * @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.
   * <p>
   *
   * @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.
   * <p>
   * 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.
   * <p>
   * 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 <code>null</code> 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 <code>null</code> 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<String>(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<String> names = new HashSet<String>();
    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<String> ignoredEntries) {
    final Set<String> names = new HashSet<String>();
    final Set<String> 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;
        }

        for(String dirName: dirNames) {
          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<String> names of entries that are dirs.
   *
   */
  static Set<String> filterDirEntries(File zip, Collection<String> names) {
    Set<String> dirs = new HashSet<String>();
    if (zip == null) {
      return dirs;
    }
    ZipFile zf = null;
    try {
      zf = new ZipFile(zip);
      for(String entryName : names) {
        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 <code>true</code> 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 <code>true</code> 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 <code>null</code> if directory).
   * @param destZip
   *          new ZIP file created.
   * @return <code>true</code> 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 <code>null</code> if directory).
   * @return <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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<String, ZipEntrySource> entryByPath = entriesByPath(entries);
    final int entryCount = entryByPath.size();
    try {
      final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip)));
      try {
        final Set<String> names = new HashSet<String>();
        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 <code>true</code> 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<String, ZipEntrySource> entryByPath = entriesByPath(entries);
    try {
      final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip)));
      try {
        // Copy and replace entries
        final Set<String> names = new HashSet<String>();
        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 (ZipEntrySource zipEntrySource : entryByPath.values()) {
          addEntry(zipEntrySource, 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<String, ZipEntrySource> entriesByPath(ZipEntrySource... entries) {
    Map<String, ZipEntrySource> result = new HashMap<String, ZipEntrySource>();
    for (int i = 0; i < entries.length; i++) {
      ZipEntrySource source = entries[i];
      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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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(Arrays.asList(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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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(Arrays.asList(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<String, ZipEntryTransformer> entryByPath;
    private final int entryCount;
    private final ZipOutputStream out;
    private final Set<String> names = new HashSet<String>();

    public TransformerZipEntryCallback(List<ZipEntryTransformerEntry> entries, ZipOutputStream out) {
      entryByPath = transformersByPath(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 <code>true</code> if at least one entry was replaced.
     */
    public boolean found() {
      return entryByPath.size() < entryCount;
    }

  }

  /**
   * @return transformers by path.
   */
  static Map<String, ZipEntryTransformer> transformersByPath(List<ZipEntryTransformerEntry> entries) {
    Map<String, ZipEntryTransformer> result = new HashMap<String, ZipEntryTransformer>();
    for (ZipEntryTransformerEntry entry: entries) {
      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 <code>true</code> if they contain same
   * entries.
   * <p>
   * 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.
   * </p>
   * <p>
   * Two archives are considered the same if
   * <ol>
   * <li>they contain same number of entries,</li>
   * <li>for each entry in the first archive there exists an entry with the same
   * in the second archive</li>
   * <li>for each entry in the first archive and the entry with the same name in
   * the second archive
   * <ol>
   * <li>both are either directories or files,</li>
   * <li>both have the same size,</li>
   * <li>both have the same CRC,</li>
   * <li>both have the same contents (compared byte-by-byte).</li>
   * </ol>
   * </li>
   * </ol>
   *
   * @param f1
   *          first ZIP file.
   * @param f2
   *          second ZIP file.
   * @return <code>true</code> if the two ZIP files contain same entries,
   *         <code>false</code> 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<? extends ZipEntry> 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.
   * <p>
   * Two entries are considered the same if
   * <ol>
   * <li>both entries exist,</li>
   * <li>both entries are either directories or files,</li>
   * <li>both entries have the same size,</li>
   * <li>both entries have the same CRC.</li>
   * </ol>
   *
   * @param path
   *          name of the entries.
   * @param e1
   *          first entry (required).
   * @param e2
   *          second entry (may be <code>null</code>).
   * @return <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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);
    }
  }

}
TOP

Related Classes of org.zeroturnaround.zip.ZipUtil$ByteArrayUnpacker

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.