001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.utils;
021
022import java.io.BufferedInputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileOutputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.util.Enumeration;
030import java.util.function.Predicate;
031import java.util.zip.ZipEntry;
032import java.util.zip.ZipFile;
033import java.util.zip.ZipInputStream;
034import java.util.zip.ZipOutputStream;
035import org.apache.commons.io.FileUtils;
036import org.apache.commons.io.IOUtils;
037
038/**
039 * Utility to zip directories and unzip files.
040 *
041 * @since 1.10
042 */
043public final class ZipUtils {
044
045  private static final String ERROR_CREATING_DIRECTORY = "Error creating directory: ";
046
047  private ZipUtils() {
048    // only static methods
049  }
050
051  /**
052   * Unzip a file into a directory. The directory is created if it does not exist.
053   *
054   * @return the target directory
055   */
056  public static File unzip(File zip, File toDir) throws IOException {
057    return unzip(zip, toDir, (Predicate<ZipEntry>) ze -> true);
058  }
059
060  public static File unzip(InputStream zip, File toDir) throws IOException {
061    return unzip(zip, toDir, (Predicate<ZipEntry>) ze -> true);
062  }
063
064  /**
065   * @deprecated replaced by {@link #unzip(InputStream, File, Predicate)} in 6.2.
066   */
067  @Deprecated
068  public static File unzip(InputStream stream, File toDir, ZipEntryFilter filter) throws IOException {
069    return unzip(stream, toDir, new ZipEntryFilterDelegate(filter));
070  }
071
072  /**
073   * Unzip a file to a directory.
074   *
075   * @param stream the zip input file
076   * @param toDir  the target directory. It is created if needed.
077   * @param filter filter zip entries so that only a subset of directories/files can be
078   *               extracted to target directory.
079   * @return the parameter {@code toDir}
080   * @since 6.2
081   */
082  public static File unzip(InputStream stream, File toDir, Predicate<ZipEntry> filter) throws IOException {
083    if (!toDir.exists()) {
084      FileUtils.forceMkdir(toDir);
085    }
086
087    ZipInputStream zipStream = new ZipInputStream(stream);
088    try {
089      ZipEntry entry;
090      while ((entry = zipStream.getNextEntry()) != null) {
091        if (filter.test(entry)) {
092          unzipEntry(entry, zipStream, toDir);
093        }
094      }
095      return toDir;
096
097    } finally {
098      zipStream.close();
099    }
100  }
101
102  private static void unzipEntry(ZipEntry entry, ZipInputStream zipStream, File toDir) throws IOException {
103    File to = new File(toDir, entry.getName());
104    if (entry.isDirectory()) {
105      throwExceptionIfDirectoryIsNotCreatable(to);
106    } else {
107      File parent = to.getParentFile();
108      throwExceptionIfDirectoryIsNotCreatable(parent);
109      copy(zipStream, to);
110    }
111  }
112
113  private static void throwExceptionIfDirectoryIsNotCreatable(File to) throws IOException {
114    if (!to.exists() && !to.mkdirs()) {
115      throw new IOException(ERROR_CREATING_DIRECTORY + to);
116    }
117  }
118
119  /**
120   * @deprecated replaced by {@link #unzip(File, File, Predicate)} in 6.2.
121   */
122  @Deprecated
123  public static File unzip(File zip, File toDir, ZipEntryFilter filter) throws IOException {
124    return unzip(zip, toDir, new ZipEntryFilterDelegate(filter));
125  }
126
127  /**
128   * Unzip a file to a directory.
129   *
130   * @param zip    the zip file. It must exist.
131   * @param toDir  the target directory. It is created if needed.
132   * @param filter filter zip entries so that only a subset of directories/files can be
133   *               extracted to target directory.
134   * @return the parameter {@code toDir}
135   * @since 6.2
136   */
137  public static File unzip(File zip, File toDir, Predicate<ZipEntry> filter) throws IOException {
138    if (!toDir.exists()) {
139      FileUtils.forceMkdir(toDir);
140    }
141
142    ZipFile zipFile = new ZipFile(zip);
143    try {
144      Enumeration<? extends ZipEntry> entries = zipFile.entries();
145      while (entries.hasMoreElements()) {
146        ZipEntry entry = entries.nextElement();
147        if (filter.test(entry)) {
148          File to = new File(toDir, entry.getName());
149          if (entry.isDirectory()) {
150            throwExceptionIfDirectoryIsNotCreatable(to);
151          } else {
152            File parent = to.getParentFile();
153            throwExceptionIfDirectoryIsNotCreatable(parent);
154            copy(zipFile, entry, to);
155          }
156        }
157      }
158      return toDir;
159
160    } finally {
161      zipFile.close();
162    }
163  }
164
165  private static void copy(ZipInputStream zipStream, File to) throws IOException {
166    FileOutputStream fos = null;
167    try {
168      fos = new FileOutputStream(to);
169      IOUtils.copy(zipStream, fos);
170    } finally {
171      IOUtils.closeQuietly(fos);
172    }
173  }
174
175  private static void copy(ZipFile zipFile, ZipEntry entry, File to) throws IOException {
176    FileOutputStream fos = new FileOutputStream(to);
177    InputStream input = null;
178    try {
179      input = zipFile.getInputStream(entry);
180      IOUtils.copy(input, fos);
181    } finally {
182      IOUtils.closeQuietly(input);
183      IOUtils.closeQuietly(fos);
184    }
185  }
186
187  public static void zipDir(File dir, File zip) throws IOException {
188    OutputStream out = null;
189    ZipOutputStream zout = null;
190    try {
191      out = FileUtils.openOutputStream(zip);
192      zout = new ZipOutputStream(out);
193      doZipDir(dir, zout);
194
195    } finally {
196      IOUtils.closeQuietly(zout);
197      IOUtils.closeQuietly(out);
198    }
199  }
200
201  private static void doZip(String entryName, InputStream in, ZipOutputStream out) throws IOException {
202    ZipEntry entry = new ZipEntry(entryName);
203    out.putNextEntry(entry);
204    IOUtils.copy(in, out);
205    out.closeEntry();
206  }
207
208  private static void doZip(String entryName, File file, ZipOutputStream out) throws IOException {
209    if (file.isDirectory()) {
210      entryName += "/";
211      ZipEntry entry = new ZipEntry(entryName);
212      out.putNextEntry(entry);
213      out.closeEntry();
214      File[] files = file.listFiles();
215      // java.io.File#listFiles() returns null if object is a directory (not possible here) or if
216      // an I/O error occurs (weird!)
217      if (files == null) {
218        throw new IllegalStateException("Fail to list files of directory " + file.getAbsolutePath());
219      }
220      for (File f : files) {
221        doZip(entryName + f.getName(), f, out);
222      }
223
224    } else {
225      try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
226        doZip(entryName, in, out);
227      }
228    }
229  }
230
231  private static void doZipDir(File dir, ZipOutputStream out) throws IOException {
232    File[] children = dir.listFiles();
233    if (children == null) {
234      throw new IllegalStateException("Fail to list files of directory " + dir.getAbsolutePath());
235    }
236    for (File child : children) {
237      doZip(child.getName(), child, out);
238    }
239  }
240
241  /**
242   * @see #unzip(File, File, Predicate)
243   * @deprecated replaced by {@link Predicate<ZipEntry>} in 6.2.
244   */
245  @Deprecated
246  @FunctionalInterface
247  public interface ZipEntryFilter {
248    boolean accept(ZipEntry entry);
249  }
250
251  private static class ZipEntryFilterDelegate implements Predicate<ZipEntry> {
252    private final ZipEntryFilter delegate;
253
254    private ZipEntryFilterDelegate(ZipEntryFilter delegate) {
255      this.delegate = delegate;
256    }
257
258    @Override
259    public boolean test(ZipEntry zipEntry) {
260      return delegate.accept(zipEntry);
261    }
262  }
263}