1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *      http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * © CrossWire Bible Society, 2005 - 2016
18   *
19   */
20  package org.crosswire.common.util;
21  
22  import java.io.Closeable;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.net.MalformedURLException;
28  import java.net.URI;
29  import java.util.Enumeration;
30  
31  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
32  import org.apache.commons.compress.archivers.zip.ZipFile;
33  import org.crosswire.jsword.JSMsg;
34  import org.slf4j.LoggerFactory;
35  
36  /**
37   * .
38   * 
39   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
40   * @author Joe Walker
41   */
42  public final class IOUtil {
43      /**
44       * Prevent instantiation
45       */
46      private IOUtil() {
47      }
48  
49      /**
50       * Unpack a zip file to a given directory. Honor the paths as given in the
51       * zip file.
52       *
53       * @param file
54       *            The zip file to download
55       * @param destdir
56       *            The directory to unpack up
57       * @throws IOException
58       *             If there is an file error
59       */
60      public static void unpackZip(File file, File destdir) throws IOException {
61          unpackZip(file, destdir, true);
62      }
63  
64      /**
65       * Unpack a zip file to a given directory. Honor the paths as given in the
66       * zip file.
67       *
68       * @param file
69       *            The zip file to download
70       * @param destdir
71       *            The directory to unpack up
72       * @param include
73       *            true to indicate the next arguments will be a filter that only includes what is specified.
74       * @param includeExcludes
75       *            a list of case insensitive patterns that will act as an inclusion or exclusion prefix
76       * @throws IOException
77       *            If there is an file error
78       */
79      public static void unpackZip(File file, File destdir, boolean include, String... includeExcludes) throws IOException {
80          // unpack the zip.
81          byte[] dbuf = new byte[4096];
82          ZipFile zf = null;
83          try {
84              zf = new ZipFile(file);
85              Enumeration<ZipArchiveEntry> entries = zf.getEntries();
86              while (entries.hasMoreElements()) {
87                  ZipArchiveEntry entry = entries.nextElement();
88                  String entrypath = entry.getName();
89  
90                  //check filters
91                  if (includeExcludes != null && includeExcludes.length > 0) {
92                          //if include, then we attempt to match ANY path
93                          //if exclude, then we ensure that we match NO path
94                      boolean skip = include;
95                      for (String filter : includeExcludes) {
96                          final boolean matchesPath = entrypath.toLowerCase().startsWith(filter);
97                          //for includes, ANY match counts, so we override the default of true to false to say 'not skip'
98                          if (include && matchesPath) {
99                              skip = false;
100                         }
101 
102                         //for excludes, the default of skip is false
103                         if (!include && matchesPath) {
104                             skip = true;
105                         }
106                     }
107 
108                     if (skip) {
109                         continue;
110                     }
111                 }
112 
113                 File entryFile = new File(destdir, entrypath);
114                 File parentDir = entryFile.getParentFile();
115                 // Is it already a directory ?
116                 if (!parentDir.isDirectory()) {
117                     // Create the directory and make sure it worked.
118                     if (!parentDir.mkdirs()) {
119                         // TRANSLATOR: Error condition: A directory could not be created. {0} is a placeholder for the directory
120                         throw new MalformedURLException(JSMsg.gettext("The URL {0} could not be created as a directory.", parentDir.toString()));
121                     }
122                 }
123 
124                 // write entryFile from zip to filesystem but avoid writing dir entries out as files
125                 if (!entry.isDirectory()) {
126 
127                     URI child = NetUtil.getURI(entryFile);
128 
129                     OutputStream dataOut = NetUtil.getOutputStream(child);
130                     InputStream dataIn = zf.getInputStream(entry);
131 
132                     while (true) {
133                         int count = dataIn.read(dbuf);
134                         if (count == -1) {
135                             break;
136                         }
137                         dataOut.write(dbuf, 0, count);
138                     }
139 
140                     dataOut.close();
141                 }
142             }
143         } finally {
144             IOUtil.close(zf);
145         }
146     }
147 
148     /**
149      * Get a zip entry by specification, returning a buffer of the contents.
150      * If there is an error, return a zero length buffer.
151      * 
152      * @param entrySpec This is of the form /path/to/zip!entryName
153      * @return the contents as a buffer
154      * @throws IOException 
155      */
156     public static byte[] getZipEntry(String entrySpec) throws IOException {
157         // Get the buffer
158         byte[] buffer = new byte[0];
159         String[] parts = StringUtil.split(entrySpec, '!');
160         ZipFile zipFile = null;
161         InputStream zin = null;
162         try {
163             zipFile = new ZipFile(parts[0]);
164             ZipArchiveEntry entry = zipFile.getEntry(parts[1]);
165             zin = zipFile.getInputStream(entry);
166             int size = (int) entry.getSize();
167             buffer = new byte[size];
168             // Repeatedly read until all has been read
169             int offset = 0;
170             while (offset < size) {
171                 offset += zin.read(buffer, offset, size - offset);
172             }
173 
174             if (offset != size) {
175                 log.error("Error: Could not read {} bytes, instead {}, for {} from {}", Integer.toString(size), Integer.toString(offset), parts[1], parts[0]);
176             }
177         } finally {
178             IOUtil.close(zin);
179             IOUtil.close(zipFile);
180         }
181         return buffer;
182     }
183 
184     /**
185      * Closes any {@link Closeable} object
186      *
187      * @param closeable
188      *            The zip file to close
189      */
190     public static void close(Closeable closeable) {
191         if (null != closeable) {
192             try {
193                 closeable.close();
194             } catch (IOException ex) {
195                 log.error("close", ex);
196             }
197         }
198     }
199 
200     /**
201      * The log stream
202      */
203     private static final org.slf4j.Logger log = LoggerFactory.getLogger(IOUtil.class);
204 }
205