View Javadoc
1   /*
2    * #%L
3    * Nuiton Utils
4    * %%
5    * Copyright (C) 2004 - 2010 CodeLutin
6    * %%
7    * This program is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU Lesser General Public License as 
9    * published by the Free Software Foundation, either version 3 of the 
10   * License, or (at your option) any later version.
11   * 
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Lesser Public License for more details.
16   * 
17   * You should have received a copy of the GNU General Lesser Public 
18   * License along with this program.  If not, see
19   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
20   * #L%
21   */
22  
23  package org.nuiton.util;
24  
25  import org.apache.commons.io.FileUtils;
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  
29  import java.io.BufferedInputStream;
30  import java.io.BufferedOutputStream;
31  import java.io.File;
32  import java.io.FileFilter;
33  import java.io.FileInputStream;
34  import java.io.FileOutputStream;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.io.OutputStream;
38  import java.nio.charset.Charset;
39  import java.util.ArrayList;
40  import java.util.Collection;
41  import java.util.Enumeration;
42  import java.util.List;
43  import java.util.zip.ZipEntry;
44  import java.util.zip.ZipFile;
45  import java.util.zip.ZipInputStream;
46  import java.util.zip.ZipOutputStream;
47  
48  /**
49   * Opérations sur des fichiers Zip. Compression et décompression avec ou
50   * sans filtres, scan des fichiers créés ou écrasés lors de la décompression...
51   *
52   * Created: 24 août 2006 10:13:35
53   *
54   * @author Benjamin Poussin - poussin@codelutin.com
55   *
56   */
57  public class ZipUtil {
58  
59      /** Class logger. */
60      private static final Log log = LogFactory.getLog(ZipUtil.class);
61  
62      /** Taille du buffer pour les lectures/écritures. */
63      private static final int BUFFER_SIZE = 8 * 1024;
64  
65      /** Le séparateur de fichier en local. */
66      private static final String LOCAL_SEP = File.separator;
67  
68      private static final String LOCAL_SEP_PATTERN = "\\".equals(LOCAL_SEP) ?
69                                                      LOCAL_SEP + LOCAL_SEP : LOCAL_SEP;
70  
71      /** Le séparateur zip. */
72      private static final String ZIP_SEP = "/";
73  
74      private static final String ZIP_SEP_PATTERN = "/";
75  
76      /** Accept all file pattern. */
77      protected static FileFilter ALL_FILE_FILTER = new FileFilter() {
78          public boolean accept(File pathname) {
79              return true;
80          }
81      };
82  
83      /**
84       * Uncompress zipped file in targetDir.
85       *
86       * @param file      the zip source file
87       * @param targetDir the destination directory
88       * @return return last entry name
89       * @throws IOException if any problem while uncompressing
90       */
91      public static String uncompress(File file, File targetDir) throws IOException {
92          String result;
93          result = uncompressAndRename(file, targetDir, null, null);
94          return result;
95      }
96  
97      /**
98       * Uncompress zipped stream in targetDir.
99       *
100      *
101      * @param stream    the zip source stream, stream is closed before return
102      * @param targetDir the destination directory
103      * @return return last entry name
104      * @throws IOException if any problem while uncompressing
105      * @since 2.6.6
106      */
107     public static String uncompress(InputStream stream, File targetDir) throws IOException {
108         String result = uncompressAndRename(stream, targetDir, null, null);
109         return result;
110     }
111 
112     /**
113      * Uncompress zipped file in targetDir, and rename uncompressed file if
114      * necessary. If renameFrom or renameTo is null no renaming is done
115      *
116      * file in zip use / to separate directory and not begin with /
117      * each directory ended with /
118      *
119      * @param file       the zip source file
120      * @param targetDir  the destination directory
121      * @param renameFrom pattern to permit rename file before uncompress it
122      * @param renameTo   new name for file if renameFrom is applicable to it
123      *                   you can use $1, $2, ... if you have '(' ')' in renameFrom
124      * @return return last entry name
125      * @throws IOException if any problem while uncompressing
126      */
127     public static String uncompressAndRename(File file,
128                                              File targetDir,
129                                              String renameFrom,
130                                              String renameTo) throws IOException {
131         return uncompressAndRename(new FileInputStream(file), targetDir, renameFrom, renameTo);
132     }
133 
134 
135     /**
136      * Uncompress zipped stream in targetDir, and rename uncompressed file if
137      * necessary. If renameFrom or renameTo is null no renaming is done
138      *
139      * file in zip use / to separate directory and not begin with /
140      * each directory ended with /
141      *
142      * @param stream     the zip source stream, stream is closed before return
143      * @param targetDir  the destination directory
144      * @param renameFrom pattern to permit rename file before uncompress it
145      * @param renameTo   new name for file if renameFrom is applicable to it
146      *                   you can use $1, $2, ... if you have '(' ')' in renameFrom
147      * @return return last entry name
148      * @throws IOException if any problem while uncompressing
149      * @since 2.6.6
150      */
151     public static String uncompressAndRename(InputStream stream,
152                                              File targetDir,
153                                              String renameFrom,
154                                              String renameTo) throws IOException {
155         String result = "";
156         ZipInputStream in = new ZipInputStream(new BufferedInputStream(stream));
157         try {
158             ZipEntry entry;
159             while ((entry = in.getNextEntry()) != null) {
160                 String name = entry.getName();
161                 if (renameFrom != null && renameTo != null) {
162                     name = name.replaceAll(renameFrom, renameTo);
163                     if (log.isDebugEnabled()) {
164                         log.debug("rename " + entry.getName() + " → " + name);
165                     }
166                 }
167                 result = name;
168                 File target = new File(targetDir, name);
169                 if (entry.isDirectory()) {
170                     FileUtil.createDirectoryIfNecessary(target);
171                 } else {
172                     FileUtil.createDirectoryIfNecessary(target.getParentFile());
173                     OutputStream out = new BufferedOutputStream(new FileOutputStream(target));
174                     try {
175                         byte[] buffer = new byte[BUFFER_SIZE];
176                         int len;
177                         while ((len = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
178                             out.write(buffer, 0, len);
179                         }
180                     } finally {
181                         out.close();
182                     }
183                 }
184             }
185         } finally {
186             in.close();
187         }
188         return result;
189     }
190 
191     /**
192      * Compress 'includes' files in zipFile. If file in includes is directory
193      * only the directory is put in zipFile, not the file contained in directory
194      *
195      * @param zipFile  the destination zip file
196      * @param root     for all file in includes that is in this directory, then we
197      *                 remove this directory in zip entry name (aka -C for tar), can be null;
198      * @param includes the files to include in zip
199      * @throws IOException if any problem while compressing
200      */
201     public static void compressFiles(File zipFile,
202                                      File root,
203                                      Collection<File> includes) throws
204             IOException {
205         compressFiles(zipFile, root, includes, false);
206     }
207 
208     /**
209      * Compress 'includes' files in zipFile. If file in includes is directory
210      * only the directory is put in zipFile, not the file contained in directory
211      *
212      * @param zipFile   the destination zip file
213      * @param root      for all file in includes that is in this directory, then we
214      *                  remove this directory in zip entry name (aka -C for tar), can be null;
215      * @param includes  the files to include in zip
216      * @param createMD5 also create a MD5 file (zip name + .md5). MD5 file is created after zip.
217      * @throws IOException if any problem while compressing
218      */
219     public static void compressFiles(File zipFile,
220                                      File root,
221                                      Collection<File> includes,
222                                      boolean createMD5) throws IOException {
223         OutputStream oStream = new FileOutputStream(zipFile);
224 
225         // if md5 creation flag
226         if (createMD5) {
227             oStream = new MD5OutputStream(oStream);
228         }
229         try {
230             ZipOutputStream zipOStream = new ZipOutputStream(oStream);
231 
232             for (File file : includes) {
233                 String entryName = toZipEntryName(root, file);
234 
235                 // Création d'une nouvelle entrée dans le zip
236                 ZipEntry entry = new ZipEntry(entryName);
237                 entry.setTime(file.lastModified());
238                 zipOStream.putNextEntry(entry);
239 
240                 if (file.isFile() && file.canRead()) {
241                     byte[] readBuffer = new byte[BUFFER_SIZE];
242                     int bytesIn;
243                     BufferedInputStream bis = new BufferedInputStream(
244                             new FileInputStream(file), BUFFER_SIZE);
245                     try {
246                         while ((bytesIn =
247                                         bis.read(readBuffer, 0, BUFFER_SIZE)) != -1) {
248                             zipOStream.write(readBuffer, 0, bytesIn);
249                         }
250                     } finally {
251                         bis.close();
252                     }
253                 }
254                 zipOStream.closeEntry();
255             }
256             zipOStream.close();
257 
258             // if md5 creation flag
259             if (createMD5) {
260                 String md5hash = StringUtil.asHex(((MD5OutputStream) oStream).hash());
261                 File md5File = new File(zipFile.getAbsoluteFile() + ".md5");
262                 FileUtils.write(md5File, md5hash, Charset.defaultCharset());
263             }
264         } finally {
265             oStream.close();
266         }
267     }
268 
269     /**
270      * If fileOrDirectory is directory Compress recursively all file in this
271      * directory, else if is just file compress one file.
272      *
273      * Entry result name in zip start at fileOrDirectory.
274      * example: if we compress /etc/apache, entry will be apache/http.conf, ...
275      *
276      * @param zipFile         the target zip file
277      * @param fileOrDirectory the file or directory to compress
278      * @throws IOException if any problem while compressing
279      */
280     public static void compress(File zipFile,
281                                 File fileOrDirectory) throws IOException {
282         compress(zipFile, fileOrDirectory, null, false);
283     }
284 
285     /**
286      * If fileOrDirectory is directory Compress recursively all file in this
287      * directory, else if is just file compress one file.
288      *
289      * Entry result name in zip start at fileOrDirectory.
290      * example: if we compress /etc/apache, entry will be apache/http.conf, ...
291      *
292      * @param zipFile         the target zip file
293      * @param fileOrDirectory the file or directory to compress
294      * @param filter          used to accept file, if null, all file is accepted
295      * @throws IOException if any problem while compressing
296      */
297     public static void compress(File zipFile,
298                                 File fileOrDirectory,
299                                 FileFilter filter) throws IOException {
300         compress(zipFile, fileOrDirectory, filter, false);
301     }
302 
303     /**
304      * If fileOrDirectory is directory Compress recursively all file in this
305      * directory, else if is just file compress one file.
306      *
307      * Entry result name in zip start at fileOrDirectory.
308      * example: if we compress /etc/apache, entry will be apache/http.conf, ...
309      *
310      * @param zipFile         the target zip file
311      * @param fileOrDirectory the file or directory to compress
312      * @param filter          used to accept file, if null, all file is accepted
313      * @param createMD5       also create a MD5 file (zip name + .md5). MD5 file is created after zip.
314      * @throws IOException if any problem while compressing
315      */
316     public static void compress(File zipFile,
317                                 File fileOrDirectory,
318                                 FileFilter filter,
319                                 boolean createMD5) throws IOException {
320         if (filter == null) {
321             filter = ALL_FILE_FILTER;
322         }
323         List<File> files = new ArrayList<File>();
324         if (fileOrDirectory.isDirectory()) {
325             files = FileUtil.getFilteredElements(fileOrDirectory, filter, true);
326         } else if (filter.accept(fileOrDirectory)) {
327             files.add(fileOrDirectory);
328         }
329 
330         compressFiles(zipFile, fileOrDirectory.getParentFile(), files,
331                       createMD5);
332     }
333 
334     /**
335      * <li> supprime le root du fichier
336      * <li> Converti les '\' en '/' car les zip entry utilise des '/'
337      * <li> ajoute un '/' a la fin pour les repertoires
338      * <li> supprime le premier '/' si la chaine commence par un '/'
339      *
340      * @param root the root directory
341      * @param file the file to treate
342      * @return the zip entry name corresponding to the given {@code file}
343      *         from {@code root} dir.
344      */
345     private static String toZipEntryName(File root, File file) {
346         String result = file.getPath();
347 
348         if (root != null) {
349             String rootPath = root.getPath();
350             if (result.startsWith(rootPath)) {
351                 result = result.substring(rootPath.length());
352             }
353         }
354 
355         result = result.replace('\\', '/');
356         if (file.isDirectory()) {
357             result += '/';
358         }
359         while (result.startsWith("/")) {
360             result = result.substring(1);
361         }
362         return result;
363     }
364 
365     /**
366      * Scan a zipFile, and fill two lists of relative paths corresponding of
367      * zip entries.
368      * First list contains all entries to be added while a uncompress operation
369      * on the destination directory {@code targetDir}.
370      * Second list contains all entries to be overwritten while a uncompress
371      * operation on the destination directory {@code targetDir}.
372      *
373      * If {@code targetDir} is {@code null} we don't fill {@code existingFiles} list.
374      *
375      * @param zipFile       location of the zip to scanZip
376      * @param targetDir     location of destination for a uncompress operation.
377      *                      If {@code null} we don't test to
378      *                      find overwritten files.
379      * @param newFiles      list of files to be added while a uncompress
380      * @param existingFiles list of files to be overwritten while a uncompress
381      *                      if the {@code targetDir},
382      *                      (only use if {@code targetDir} is not
383      *                      {@code null})
384      * @param excludeFilter used to exclude some files
385      * @param renameFrom    {@link #uncompressAndRename(File, File, String, String)}
386      * @param renameTo      {@link #uncompressAndRename(File, File, String, String)}
387      * @throws IOException if any exception while dealing with zipfile
388      */
389     public static void scan(File zipFile,
390                             File targetDir,
391                             List<String> newFiles,
392                             List<String> existingFiles,
393                             FileFilter excludeFilter,
394                             String renameFrom,
395                             String renameTo) throws IOException {
396         ZipFile zip = null;
397         try {
398             zip = new ZipFile(zipFile);
399             boolean findExisting = targetDir != null && targetDir.exists();
400             boolean filter = findExisting && excludeFilter != null;
401             boolean rename = renameFrom != null && renameTo != null;
402             Enumeration<? extends ZipEntry> entries = zip.entries();
403             while (entries.hasMoreElements()) {
404                 String entryName = entries.nextElement().getName();
405                 if (rename) {
406                     entryName = entryName.replaceAll(renameFrom, renameTo);
407                 }
408                 String name = convertToLocalEntryName(entryName);
409                 if (findExisting || filter) {
410                     File file = new File(targetDir, name);
411                     if (filter && excludeFilter.accept(file)) continue;
412                     if (file.exists()) {
413                         existingFiles.add(name);
414                         continue;
415                     }
416                 }
417                 newFiles.add(name);
418             }
419         } finally {
420             if (zip != null) {
421                 zip.close();
422             }
423         }
424     }
425 
426     @SuppressWarnings({"unchecked"})
427     public static List<String>[] scanAndExplodeZip(File source,
428                                                    File root,
429                                                    FileFilter excludeFilter)
430             throws IOException {
431 
432         List<String> overwrittenFiles = new ArrayList<String>();
433         List<String> newFiles = new ArrayList<String>();
434 
435         // obtain list of relative paths (to add or overwrite)
436         scan(source, root, newFiles, overwrittenFiles, excludeFilter, null,
437              null);
438 
439         return new List[]{newFiles, overwrittenFiles};
440     }
441 
442     /**
443      * uncompress zipped file in targetDir.
444      *
445      * If {@code toTreate} if not null nor empty, we use it to filter
446      * entries to uncompress : it contains a list of relative local path of
447      * files to uncompress.
448      * Otherwise just delegate to {@link ZipUtil#uncompress(File, File)}.
449      *
450      * @param file       location of zip file
451      * @param targetDir  destination directory
452      * @param toTreate   list of relative local path of entries to treate
453      * @param renameFrom {@link #uncompressAndRename(File, File, String, String)}
454      * @param renameTo   {@link #uncompressAndRename(File, File, String, String)}
455      * @return return last entry name
456      * @throws IOException if nay exception while operation
457      */
458     public static String uncompress(File file,
459                                     File targetDir,
460                                     List<String> toTreate,
461                                     String renameFrom,
462                                     String renameTo) throws IOException {
463         if (toTreate == null || toTreate.isEmpty()) {
464             return uncompressAndRename(file, targetDir, renameFrom, renameTo);
465         }
466 
467         boolean rename = renameFrom != null && renameTo != null;
468 
469         String result = "";
470         ZipEntry entry;
471         ZipInputStream in = new ZipInputStream(new FileInputStream(file));
472         try {
473             while ((entry = in.getNextEntry()) != null) {
474                 String name = entry.getName();
475                 if (rename) {
476                     result = convertToLocalEntryName(name.replaceAll(renameFrom,
477                                                                      renameTo));
478                 } else {
479                     result = convertToLocalEntryName(name);
480                 }
481 
482                 if (log.isDebugEnabled()) {
483                     log.debug("open [" + name + "] : " + result);
484                 }
485                 if (!toTreate.contains(result)) {
486                     continue;
487                 }
488 
489                 if (log.isDebugEnabled()) {
490                     log.debug("copy [" + name + "] : " + result);
491                 }
492                 File target = new File(targetDir, result);
493                 if (entry.isDirectory()) {
494                     FileUtil.createDirectoryIfNecessary(target);
495                 } else {
496                     FileUtil.createDirectoryIfNecessary(target.getParentFile());
497                     OutputStream out = new BufferedOutputStream(
498                             new FileOutputStream(target));
499                     try {
500                         byte[] buffer = new byte[BUFFER_SIZE];
501                         int len;
502                         while ((len = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
503                             out.write(buffer, 0, len);
504                         }
505                     } finally {
506                         out.close();
507                     }
508                 }
509             }
510         } finally {
511             in.close();
512         }
513         return result;
514     }
515 
516     /**
517      * Unzip compressed archive and keep non excluded patterns.
518      *
519      * @param file      archive file
520      * @param targetDir destination file
521      * @param excludes  excludes pattern (pattern must match complete entry name including root folder)
522      * @throws IOException FIXME
523      */
524     public static void uncompressFiltred(File file,
525                                          File targetDir,
526                                          String... excludes) throws IOException {
527 
528         ZipFile zipFile = new ZipFile(file);
529 
530         Enumeration<? extends ZipEntry> entries = zipFile.entries();
531 
532         while (entries.hasMoreElements()) {
533             ZipEntry entry = entries.nextElement();
534 
535             String name = entry.getName();
536             // add continue to break loop
537             boolean excludeEntry = false;
538             if (excludes != null) {
539                 for (String exclude : excludes) {
540                     if (name.matches(exclude)) {
541                         excludeEntry = true;
542                     }
543                 }
544             }
545 
546             if (!excludeEntry) {
547                 File target = new File(targetDir, name);
548                 if (entry.isDirectory()) {
549                     FileUtil.createDirectoryIfNecessary(target);
550                 } else {
551                     // get inputstream only here
552                     FileUtil.createDirectoryIfNecessary(target.getParentFile());
553                     InputStream in = zipFile.getInputStream(entry);
554                     try {
555                         OutputStream out = new BufferedOutputStream(
556                                 new FileOutputStream(target));
557                         try {
558                             byte[] buffer = new byte[8 * 1024];
559                             int len;
560 
561                             while ((len = in.read(buffer, 0, 8 * 1024)) != -1) {
562                                 out.write(buffer, 0, len);
563                             }
564                         } finally {
565                             out.close();
566                         }
567                     } finally {
568                         in.close();
569                     }
570                 }
571             }
572         }
573     }
574 
575     /**
576      * Tests if the given file is a zip file.
577      *
578      * @param file the file to test
579      * @return {@code true} if the file is a valid zip file,
580      *         {@code false} otherwise.
581      * @since 2.4.9
582      */
583     public static boolean isZipFile(File file) {
584 
585         boolean result = false;
586         try {
587             ZipFile zipFile = new ZipFile(file);
588             zipFile.close();
589             result = true;
590         } catch (IOException e) {
591             // silent test
592         }
593         return result;
594     }
595 
596     protected static String convertToLocalEntryName(String txt) {
597         String s = txt.replaceAll(ZIP_SEP_PATTERN, LOCAL_SEP_PATTERN);
598         if (s.endsWith(ZIP_SEP)) {
599             s = s.substring(0, s.length() - 1);
600         }
601         return s;
602     }
603 }