1   package org.daisy.pipeline.zip.calabash.impl;
2   
3   /**
4    * Unzip extension for non-XML files
5    *
6    * @author Lars Wittmar -- le-tex publishing services GmbH
7    * @date   2012-03-08
8    * 
9    * A Java unzip for XProc, particularly useful 
10   * if you want to access zip contents as extracted files
11   * (and don't want to retrieve single files recursively, and
12   * potentially base64 encoded).
13   * 
14   * Written by Lars Wittmar, le-tex publishing services GmbH
15   * (small portions also by Gerrit Imsieke)
16   * 
17   * Configure letex:unzip step by passing options to it:
18   * zip (required):      file name (not URI) of a zip file
19   * dest-dir (required): directory (relative or absolute; not an URI)
20   * overwrite ('yes'/'no', optional, default 'no'): overwrite individual files even if they already exist
21   * file (optional):     file name relative to zip root. If omitted, extract complete zip file
22   * 
23   * The directory (and missing subdirectories) will be created if
24   * missing. 
25   * 
26   * CAVEAT: If overwrite='yes' and, e.g., dest-dir='/', it will attempt to remove the root directory.
27   * 
28   * DO NOT USE THIS AS A USER WITH ADMIN PRIVILEGES.
29   * 
30   *
31   * 2015-02-27, Jostein Austvik Jacobsen (NLB - Norwegian library of talking books and braille):
32   *     - Wrapped into a org.daisy.common.xproc.calabash.XProcStepProvider
33   *     - removed Apache Commons dependency
34   *     - Use file URIs instead of OS-specific paths for options
35   * 2016-02-10, Jostein Austvik Jacobsen (NLB - Norwegian library of talking books and braille):
36   *     - slightly changed the meaning of the 'overwrite' option
37   */
38  
39  import java.io.BufferedInputStream;
40  import java.io.BufferedOutputStream;
41  import java.io.File;
42  import java.io.FileInputStream;
43  import java.io.FileOutputStream;
44  import java.io.PrintWriter;
45  import java.io.StringReader;
46  import java.io.StringWriter;
47  import java.net.URI;
48  import java.util.Map;
49  import java.util.zip.ZipEntry;
50  import java.util.zip.ZipInputStream;
51  
52  import javax.xml.transform.Source;
53  import javax.xml.transform.stream.StreamSource;
54  
55  import com.xmlcalabash.core.XProcException;
56  import com.xmlcalabash.core.XProcRuntime;
57  import com.xmlcalabash.io.WritablePipe;
58  import com.xmlcalabash.library.DefaultStep;
59  import com.xmlcalabash.model.RuntimeValue;
60  import com.xmlcalabash.runtime.XAtomicStep;
61  
62  import net.sf.saxon.s9api.DocumentBuilder;
63  import net.sf.saxon.s9api.SaxonApiException;
64  import net.sf.saxon.s9api.XdmNode;
65  import net.sf.saxon.s9api.QName;
66  
67  import org.daisy.common.xproc.calabash.XProcStep;
68  import org.daisy.common.xproc.calabash.XProcStepProvider;
69  import org.daisy.common.xproc.XProcMonitor;
70  
71  import org.osgi.service.component.annotations.Component;
72  
73  import org.slf4j.Logger;
74  import org.slf4j.LoggerFactory;
75  
76  @Component(
77  	name = "letex:unzip",
78  	service = { XProcStepProvider.class },
79  	property = { "type:String={http://www.le-tex.de/namespace}unzip" }
80  )
81  public class UnZipProvider implements XProcStepProvider {
82  
83      /** The logger. */
84      Logger logger = LoggerFactory.getLogger(this.getClass());
85  
86      /* (non-Javadoc)
87       * @see org.daisy.common.xproc.calabash.XProcStepProvider#newStep(com.xmlcalabash.core.XProcRuntime, com.xmlcalabash.runtime.XAtomicStep)
88       */
89      @Override
90      public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
91          return new UnZip(runtime, step);
92      }
93  
94      /**
95       * Activate (OSGI)
96       */
97      public void activate() {
98          logger.trace("Activating letex:unzip provider");
99      }
100     
101     public static class UnZip
102             extends DefaultStep implements XProcStep
103     {
104         public UnZip(XProcRuntime runtime, XAtomicStep step)
105         {
106             super(runtime,step);
107         }
108 
109         @Override
110         public void setOutput(String port, WritablePipe pipe)
111         {
112             myResult = pipe;
113         }
114 
115         @Override
116         public void reset()
117         {
118             myResult.resetWriter();
119         }
120 
121         @Override
122         public void run()
123             throws SaxonApiException
124         {
125             super.run();
126             
127             String result = null;
128             File folder = null, source = null;
129             try {
130                 RuntimeValue href = getOption(new QName("zip"));
131                 URI sourceUri = href.getBaseURI().resolve(href.getString());
132                 href = getOption(new QName("dest-dir"));
133                 URI destinationUri = href.getBaseURI().resolve(href.getString());
134     			
135     			if (!"file".equals(sourceUri.getScheme())) {
136     				throw new XProcException(step, "Only file: scheme URIs are supported by zip URI.");
137     			} else {
138     				source = new File(sourceUri.getPath());
139     				if (!source.isFile()) {
140     					throw new XProcException(step, "The zip URI must refer to a file.");
141     				}
142     			}
143     			
144     			if (!"file".equals(destinationUri.getScheme())) {
145     				throw new XProcException(step, "Only file: scheme URIs are supported by dest-dir URI.");
146     			} else {
147     				folder = new File(destinationUri.getPath());
148     				if (!folder.exists()) {
149     					folder.mkdirs();
150     				}
151     				if (!folder.isDirectory()) {
152     					throw new XProcException(step, "The dest-dir URI must refer to a directory.");
153     				}
154     				if (folder.getParentFile() == null || folder.getParentFile().getParentFile() == null) {
155     					// Basic safety valve; should catch stuff like "/", "/home/", "C:\", "C:\Windows\" etc.
156     					// Mainly aimed at catching typos or bugs in scripts during developent;
157     					// running without admin-privileges is the only fairly safe option here 
158     					throw new XProcException(step, "The dest-dir URI must not be the file system root directory (or any of its children) (was: \""+folder.getCanonicalPath()+"\").");
159     				}
160     			}
161     			
162                 String overwriteS = getOption(new QName("overwrite")).getString();
163                 boolean overwrite = false; 
164                 if (overwriteS != null) {
165                     if (overwriteS.matches("yes")) {
166                         overwrite = true;
167                     }
168                 }
169 
170     			String filename = "";
171     			if (getOption(new QName("file"))!=null) {
172                     filename = getOption(new QName("file")).getString();
173     			}
174     			
175                 if(! folder.exists()) { 
176                     folder.mkdirs(); 
177                 }
178     			
179     			final int BUFFER = 2048;
180     			BufferedOutputStream dest = null;
181     			FileInputStream fis = new FileInputStream(source);
182     			ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
183     			ZipEntry entry;
184 
185                 int numFiles = 0;
186     			result = "<c:files xmlns:c=\"http://www.w3.org/ns/xproc-step\" xml:base=\"" + folder.toURI() + "\">";
187     			//without given fileNode extract everything
188     			if(filename.equals("")) {
189                     while((entry = zis.getNextEntry()) != null) {
190                         int count;
191                         byte data[] = new byte[BUFFER];
192                         
193                         if(entry.isDirectory()) {
194                         	File dir = new File(folder, entry.getName());
195                             if (!dir.exists()) { dir.mkdirs(); }
196                         } else {
197                             numFiles++;
198                             result += "<c:file name=\"" + entry.getName() + "\"/>";
199                             File destFile = new File(folder, entry.getName());
200                             if (destFile.exists()) {
201                                 if (overwrite) {
202                                     deleteDirectory(destFile);
203                                 } else {
204                                     continue;
205                                 }
206                             }
207                             File destdir = destFile.getParentFile();
208                             if (!destdir.exists()) { destdir.mkdirs(); }
209                             FileOutputStream fos = new FileOutputStream(destFile);
210                             dest = new BufferedOutputStream(fos, BUFFER);
211                             while ((count = zis.read(data, 0, BUFFER)) != -1) {
212                                 dest.write(data, 0, count);
213                             }
214                             dest.flush();
215                             dest.close();
216                         }
217                     }
218     			} else {
219                     while((entry = zis.getNextEntry()) != null) {
220                         if(filename.equals(entry.getName())) {
221                             int count;
222                             byte data[] = new byte[BUFFER];
223                             
224                             numFiles++;
225                             result += "<c:file name=\"" + entry.getName() + "\"/>";
226                             File destFile = new File(folder, entry.getName());
227                             if (destFile.exists()) {
228                                 if (overwrite) {
229                                     deleteDirectory(destFile);
230                                 } else {
231                                     continue;
232                                 }
233                             }
234                             File destdir = destFile.getParentFile();
235                             if (!destdir.exists()) { destdir.mkdirs(); }
236                             FileOutputStream fos = new FileOutputStream(destFile);
237                             dest = new BufferedOutputStream(fos, BUFFER);
238                             while ((count = zis.read(data, 0, BUFFER)) != -1) {
239                                 dest.write(data, 0, count);
240                             }
241                             dest.flush();
242                             dest.close();
243                         }							 
244                     }
245     			}
246     			
247     			zis.close();
248     			if (numFiles == 0) {
249     			  result = "<c:error xmlns:c=\"http://www.w3.org/ns/xproc-step\" xmlns:letex=\"http://www.le-tex.de/namespace\" code=\"zip-error\" href=\""+folder.toURI()+source.toURI()+"\">No content processed. Zip file may empty or corrupted.</c:error>";
250     			} else {
251     			  result += "</c:files>";
252     			}
253                 
254     		  } catch(Exception e) {
255     			  StringWriter stackTraceWriter = new StringWriter();
256     			  e.printStackTrace(new PrintWriter(stackTraceWriter));
257     			  String stackTrace = stackTraceWriter.toString().replaceAll("-", " - "); // just to be sure that the comment is valid
258     			  result = "<c:error xmlns:c=\"http://www.w3.org/ns/xproc-step\" xmlns:letex=\"http://www.le-tex.de/namespace\" code=\"zip-error\" href=\""+folder.toURI()+source.toURI()+"\">Zip file seems to be corrupted: "+e.getMessage()+"<!-- "+stackTrace+" --></c:error>";
259     		  }
260             DocumentBuilder builder = runtime.getProcessor().newDocumentBuilder();
261             Source src = new StreamSource(new StringReader(result));
262             XdmNode doc = builder.build(src);
263 
264             myResult.write(doc);
265         }
266         
267         /** http://stackoverflow.com/a/3775718/281065 */
268         public static boolean deleteDirectory(File directory) {
269             if(directory.exists()){
270                 File[] files = directory.listFiles();
271                 if(null!=files){
272                     for(int i=0; i<files.length; i++) {
273                         if(files[i].isDirectory()) {
274                             deleteDirectory(files[i]);
275                         }
276                         else {
277                             files[i].delete();
278                         }
279                     }
280                 }
281             }
282             return(directory.delete());
283         }
284 
285         private WritablePipe myResult = null;
286     }
287     
288 }