1   package com.atlassian.maven.plugins.amps.util;
2   
3   import org.apache.commons.io.IOUtils;
4   import org.apache.commons.lang.StringUtils;
5   
6   import com.google.common.collect.Lists;
7   
8   
9   import java.io.File;
10  import java.io.FileInputStream;
11  import java.io.FileOutputStream;
12  import java.io.IOException;
13  import java.io.InputStream;
14  import java.io.OutputStream;
15  import java.util.Enumeration;
16  import java.util.List;
17  import java.util.zip.ZipEntry;
18  import java.util.zip.ZipException;
19  import java.util.zip.ZipFile;
20  import java.util.zip.ZipOutputStream;
21  
22  public class ZipUtils
23  {
24      public static void unzip(final File zipFile, final String destDir) throws IOException
25      {
26          unzip(zipFile, destDir, 0);
27      }
28  
29      /**
30       * Unzips a file
31       *
32       * @param zipFile
33       *            the Zip file
34       * @param destDir
35       *            the destination folder
36       * @param leadingPathSegmentsToTrim
37       *            number of root folders to skip. Example: If all files are in generated-resources/home/*,
38       *            then you may want to skip 2 folders.
39       * @throws IOException
40       */
41      public static void unzip(final File zipFile, final String destDir, int leadingPathSegmentsToTrim) throws IOException
42      {
43          final ZipFile zip = new ZipFile(zipFile);
44          try
45          {
46              final Enumeration<? extends ZipEntry> entries = zip.entries();
47              while (entries.hasMoreElements())
48              {
49                  final ZipEntry zipEntry = entries.nextElement();
50                  String zipPath = trimPathSegments(zipEntry.getName(), leadingPathSegmentsToTrim);
51                  final File file = new File(destDir + "/" + zipPath);
52                  if (zipEntry.isDirectory())
53                  {
54                      file.mkdirs();
55                      continue;
56                  }
57                  // make sure our parent exists in case zipentries are out of order
58                  if (!file.getParentFile().exists())
59                  {
60                      file.getParentFile().mkdirs();
61                  }
62  
63                  InputStream is = null;
64                  OutputStream fos = null;
65                  try
66                  {
67                      is = zip.getInputStream(zipEntry);
68                      fos = new FileOutputStream(file);
69                      IOUtils.copy(is, fos);
70                  }
71                  finally
72                  {
73                      IOUtils.closeQuietly(is);
74                      IOUtils.closeQuietly(fos);
75                  }
76                  file.setLastModified(zipEntry.getTime());
77              }
78          }
79          finally
80          {
81              try
82              {
83                  zip.close();
84              }
85              catch (IOException e)
86              {
87                  // ignore
88              }
89          }
90      }
91  
92      /**
93       * Count the number of nested root folders. A root folder is a folder which contains 0 or 1 file or folder.
94       *
95       * Example: A zip with only "generated-resources/home/database.log" has 2 root folders.
96       *
97       * @param zip the zip file
98       * @return the number of root folders.
99       */
100     public static int countNestingLevel(File zip) throws ZipException, IOException
101     {
102         List<String> filenames = toList(new ZipFile(zip).entries());
103         return countNestingLevel(filenames);
104     }
105 
106     /**
107      * Count the number of nested root directories in the filenames.
108      *
109      * A root directory is a directory that has no sibling.
110      * @param filenames the list of filenames, using / as a separator. Must be a mutable copy,
111      * as it will be modified.
112      */
113     static int countNestingLevel(List<String> filenames)
114     {
115         String prefix = StringUtils.getCommonPrefix(filenames.toArray(new String[filenames.size()]));
116         if (!prefix.endsWith("/"))
117         {
118             prefix = prefix.substring(0, prefix.lastIndexOf("/") + 1);
119         }
120 
121         // The first prefix may be wrong, example:
122         // root/ <- to be discarded
123         // root/nested/ <- to be discarded
124         // root/nested/folder1/file.txt <- the root "root/nested/" will be detected properly
125         // root/nested/folder2/file.txt
126         if (filenames.remove(prefix))
127         {
128             return countNestingLevel(filenames);
129         }
130 
131         // The client can't use these filenames anymore.
132         filenames.clear();
133         return StringUtils.countMatches(prefix, "/");
134     }
135 
136     private static List<String> toList(final Enumeration<? extends ZipEntry> entries)
137     {
138         List<String> filenamesList = Lists.newArrayList();
139         while (entries.hasMoreElements())
140         {
141             final ZipEntry zipEntry = entries.nextElement();
142             filenamesList.add(zipEntry.getName());
143         }
144         return filenamesList;
145     }
146 
147     /**
148      * @param prefix the prefix. If empty, uses the srcDir's name. That means you can't create a zip with no
149      * root folder.
150      */
151     public static void zipDir(final File zipFile, final File srcDir, final String prefix) throws IOException
152     {
153         ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));
154         try
155         {
156             addZipPrefixes(srcDir, out, prefix);
157             addZipDir(srcDir, out, prefix);
158         }
159         finally
160         {
161             // Complete the ZIP file
162             IOUtils.closeQuietly(out);
163         }
164     }
165 
166     private static void addZipPrefixes(File dirObj, ZipOutputStream out, String prefix) throws IOException
167     {
168         // need to manually add the prefix folders
169         String entryPrefix = ensurePrefixWithSlash(dirObj, prefix);
170 
171         String[] prefixes = entryPrefix.split("/");
172         String lastPrefix = "";
173         for (int i = 0; i < prefixes.length; i++)
174         {
175             ZipEntry entry = new ZipEntry(lastPrefix + prefixes[i] + "/");
176             out.putNextEntry(entry);
177             out.closeEntry();
178 
179             lastPrefix = prefixes[i] + "/";
180         }
181     }
182 
183     private static void addZipDir(File dirObj, ZipOutputStream out, String prefix) throws IOException
184     {
185         File[] files = dirObj.listFiles();
186         byte[] tmpBuf = new byte[1024];
187         File currentFile;
188         String entryPrefix = ensurePrefixWithSlash(dirObj, prefix);
189         String entryName = "";
190 
191         for (int i = 0; i < files.length; i++)
192         {
193             currentFile = files[i];
194             if (currentFile.isDirectory())
195             {
196                 entryName = entryPrefix + currentFile.getName() + "/";
197 
198                 // need to manually add folders so entries are in order
199                 ZipEntry entry = new ZipEntry(entryName);
200                 out.putNextEntry(entry);
201                 out.closeEntry();
202 
203                 // add the files in the folder
204                 addZipDir(currentFile, out, entryName);
205             }
206             else if (currentFile.isFile())
207             {
208 
209                 entryName = entryPrefix + currentFile.getName();
210                 FileInputStream in = new FileInputStream(currentFile.getAbsolutePath());
211                 try
212                 {
213                     out.putNextEntry(new ZipEntry(entryName));
214                     // Transfer from the file to the ZIP file
215                     int len;
216                     while ((len = in.read(tmpBuf)) > 0)
217                     {
218                         out.write(tmpBuf, 0, len);
219                     }
220 
221                     // Complete the entry
222                     out.closeEntry();
223                 }
224                 finally
225                 {
226                     IOUtils.closeQuietly(in);
227                 }
228             }
229         }
230     }
231 
232     /**
233      * Make sure 'prefix' is in format 'entry/' or, by default, 'rootDir/'
234      * (not '', '/', '/entry', or 'entry').
235      */
236     private static String ensurePrefixWithSlash(File rootDir, String prefix)
237     {
238         String entryPrefix = prefix;
239 
240         if (StringUtils.isNotBlank(entryPrefix) && !entryPrefix.equals("/"))
241         {
242             // strip leading '/'
243             if (entryPrefix.charAt(0) == '/')
244             {
245                 entryPrefix = entryPrefix.substring(1);
246             }
247             // ensure trailing '/'
248             if (entryPrefix.charAt(entryPrefix.length() - 1) != '/')
249             {
250                 entryPrefix = entryPrefix + "/";
251             }
252         }
253         else
254         {
255             entryPrefix = rootDir.getName() + "/";
256         }
257 
258         return entryPrefix;
259     }
260 
261     private static String trimPathSegments(String zipPath, final int trimLeadingPathSegments)
262     {
263         int startIndex = 0;
264         for (int i = 0; i < trimLeadingPathSegments; i++)
265         {
266             int nextSlash = zipPath.indexOf("/", startIndex);
267             if (nextSlash == -1)
268             {
269                 break;
270             }
271             else
272             {
273                 startIndex = nextSlash + 1;
274             }
275         }
276 
277         return zipPath.substring(startIndex);
278     }
279 
280 }