/** * 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 com.smartandroid.sa.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.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 com.smartandroid.sa.zip.commons.FileUtils; import com.smartandroid.sa.zip.commons.FilenameUtils; import com.smartandroid.sa.zip.commons.IOUtils; import com.smartandroid.sa.zip.transform.ZipEntryTransformer; import com.smartandroid.sa.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 } 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. */ 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. * * @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. * * @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("Unwraping '{}' 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("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. * <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 = 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. * <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. */ 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 = 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. * <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 = 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. * <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) { e.printStackTrace(); // 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); } } }