JAL-1286 URL stream reader tries to use a GZIPInputStream if URL ends in .gz
[jalview.git] / src / jalview / io / FileParse.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
3  * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
10  *  
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.io;
19
20 import java.io.*;
21 import java.net.*;
22 import java.util.zip.GZIPInputStream;
23
24 /**
25  * implements a random access wrapper around a particular datasource, for
26  * passing to identifyFile and AlignFile objects.
27  */
28 public class FileParse
29 {
30   /**
31    * text specifying source of data. usually filename or url.
32    */
33   private String dataName = "unknown source";
34
35   public File inFile = null;
36
37   public int index = 1; // sequence counter for FileParse object created from
38
39   // same data source
40
41   protected char suffixSeparator = '#';
42
43   /**
44    * character used to write newlines
45    */
46   protected String newline = System.getProperty("line.separator");
47
48   public void setNewlineString(String nl)
49   {
50     newline = nl;
51   }
52
53   public String getNewlineString()
54   {
55     return newline;
56   }
57
58   /**
59    * '#' separated string tagged on to end of filename or url that was clipped
60    * off to resolve to valid filename
61    */
62   protected String suffix = null;
63
64   protected String type = null;
65
66   protected BufferedReader dataIn = null;
67
68   protected String errormessage = "UNITIALISED SOURCE";
69
70   protected boolean error = true;
71
72   protected String warningMessage = null;
73
74   /**
75    * size of readahead buffer used for when initial stream position is marked.
76    */
77   final int READAHEAD_LIMIT = 2048;
78
79   public FileParse()
80   {
81   }
82
83   /**
84    * Create a new FileParse instance reading from the same datasource starting
85    * at the current position. WARNING! Subsequent reads from either object will
86    * affect the read position of the other, but not the error state.
87    * 
88    * @param from
89    */
90   public FileParse(FileParse from) throws IOException
91   {
92     if (from == null)
93     {
94       throw new Error(
95               "Implementation error. Null FileParse in copy constructor");
96     }
97     if (from == this)
98       return;
99     index = ++from.index;
100     inFile = from.inFile;
101     suffixSeparator = from.suffixSeparator;
102     suffix = from.suffix;
103     errormessage = from.errormessage; // inherit potential error messages
104     error = false; // reset any error condition.
105     type = from.type;
106     dataIn = from.dataIn;
107     if (dataIn != null)
108     {
109       mark();
110     }
111     dataName = from.dataName;
112   }
113
114   /**
115    * Attempt to open a file as a datasource. Sets error and errormessage if
116    * fileStr was invalid.
117    * 
118    * @param fileStr
119    * @return this.error (true if the source was invalid)
120    */
121   private boolean checkFileSource(String fileStr) throws IOException
122   {
123     error = false;
124     this.inFile = new File(fileStr);
125     // check to see if it's a Jar file in disguise.
126     if (!inFile.exists())
127     {
128       errormessage = "FILE NOT FOUND";
129       error = true;
130     }
131     if (!inFile.canRead())
132     {
133       errormessage = "FILE CANNOT BE OPENED FOR READING";
134       error = true;
135     }
136     if (inFile.isDirectory())
137     {
138       // this is really a 'complex' filetype - but we don't handle directory
139       // reads yet.
140       errormessage = "FILE IS A DIRECTORY";
141       error = true;
142     }
143     if (!error)
144     {
145       dataIn = new BufferedReader(new FileReader(fileStr));
146       dataName = fileStr;
147     }
148     return error;
149   }
150
151   private boolean checkURLSource(String fileStr) throws IOException,
152           MalformedURLException
153   {
154     errormessage = "URL NOT FOUND";
155     URL url = new URL(fileStr);
156     //
157     // GZIPInputStream code borrowed from Aquaria (soon to be open sourced) via Kenny Sabir
158     Exception e=null;
159     if (fileStr.endsWith(".gz")) {
160       try {
161           InputStream inputStream = url.openStream();
162           dataIn = new BufferedReader(new InputStreamReader(new GZIPInputStream(inputStream)));
163           dataIn.mark(2048);
164           dataIn.read();
165           dataIn.reset();
166           
167           dataName = fileStr;
168           return false;
169       } catch (Exception ex) {
170         e=ex;
171       }
172     }
173
174     try {
175       dataIn = new BufferedReader(new InputStreamReader(url.openStream()));
176     } catch (IOException q) {
177       if (e!=null)
178       {
179         throw new IOException("Failed to resolve GZIP stream", e);
180       }
181       throw q;
182     }
183     // record URL as name of datasource.
184     dataName = fileStr;
185     return false;
186   }
187
188   /**
189    * sets the suffix string (if any) and returns remainder (if suffix was
190    * detected)
191    * 
192    * @param fileStr
193    * @return truncated fileStr or null
194    */
195   private String extractSuffix(String fileStr)
196   {
197     // first check that there wasn't a suffix string tagged on.
198     int sfpos = fileStr.lastIndexOf(suffixSeparator);
199     if (sfpos > -1 && sfpos < fileStr.length() - 1)
200     {
201       suffix = fileStr.substring(sfpos + 1);
202       // System.err.println("DEBUG: Found Suffix:"+suffix);
203       return fileStr.substring(0, sfpos);
204     }
205     return null;
206   }
207
208   /**
209    * Create a datasource for input to Jalview. See AppletFormatAdapter for the
210    * types of sources that are handled.
211    * 
212    * @param fileStr
213    *          - datasource locator/content
214    * @param type
215    *          - protocol of source
216    * @throws MalformedURLException
217    * @throws IOException
218    */
219   public FileParse(String fileStr, String type)
220           throws MalformedURLException, IOException
221   {
222     this.type = type;
223     error = false;
224
225     if (type.equals(AppletFormatAdapter.FILE))
226     {
227       if (checkFileSource(fileStr))
228       {
229         String suffixLess = extractSuffix(fileStr);
230         if (suffixLess != null)
231         {
232           if (checkFileSource(suffixLess))
233           {
234             throw new IOException("Problem opening " + inFile
235                     + " (also tried " + suffixLess + ") : " + errormessage);
236           }
237         }
238         else
239         {
240           throw new IOException("Problem opening " + inFile + " : "
241                   + errormessage);
242         }
243       }
244     }
245     else if (type.equals(AppletFormatAdapter.URL))
246     {
247       try
248       {
249         try
250         {
251           checkURLSource(fileStr);
252           if (suffixSeparator == '#')
253             extractSuffix(fileStr); // URL lref is stored for later reference.
254         } catch (IOException e)
255         {
256           String suffixLess = extractSuffix(fileStr);
257           if (suffixLess == null)
258           {
259             throw (e);
260           }
261           else
262           {
263             try
264             {
265               checkURLSource(suffixLess);
266             } catch (IOException e2)
267             {
268               errormessage = "BAD URL WITH OR WITHOUT SUFFIX";
269               throw (e); // just pass back original - everything was wrong.
270             }
271           }
272         }
273       } catch (Exception e)
274       {
275         errormessage = "CANNOT ACCESS DATA AT URL '" + fileStr + "' ("
276                 + e.getMessage() + ")";
277         error = true;
278       }
279     }
280     else if (type.equals(AppletFormatAdapter.PASTE))
281     {
282       errormessage = "PASTE INACCESSIBLE!";
283       dataIn = new BufferedReader(new StringReader(fileStr));
284       dataName = "Paste";
285     }
286     else if (type.equals(AppletFormatAdapter.CLASSLOADER))
287     {
288       errormessage = "RESOURCE CANNOT BE LOCATED";
289       java.io.InputStream is = getClass()
290               .getResourceAsStream("/" + fileStr);
291       if (is == null)
292       {
293         String suffixLess = extractSuffix(fileStr);
294         if (suffixLess != null)
295           is = getClass().getResourceAsStream("/" + suffixLess);
296       }
297       if (is != null)
298       {
299         dataIn = new BufferedReader(new java.io.InputStreamReader(is));
300         dataName = fileStr;
301       }
302       else
303       {
304         error = true;
305       }
306     }
307     else
308     {
309       errormessage = "PROBABLE IMPLEMENTATION ERROR : Datasource Type given as '"
310               + (type != null ? type : "null") + "'";
311       error = true;
312     }
313     if (dataIn == null || error)
314     {
315       // pass up the reason why we have no source to read from
316       throw new IOException("Failed to read data from source:\n"
317               + errormessage);
318     }
319     error = false;
320     dataIn.mark(READAHEAD_LIMIT);
321   }
322
323   /**
324    * mark the current position in the source as start for the purposes of it
325    * being analysed by IdentifyFile().identify
326    * 
327    * @throws IOException
328    */
329   public void mark() throws IOException
330   {
331     if (dataIn != null)
332     {
333       dataIn.mark(READAHEAD_LIMIT);
334     }
335     else
336     {
337       throw new IOException("Unitialised Source Stream");
338     }
339   }
340
341   public String nextLine() throws IOException
342   {
343     if (!error)
344       return dataIn.readLine();
345     throw new IOException("Invalid Source Stream:" + errormessage);
346   }
347
348   public boolean isValid()
349   {
350     return !error;
351   }
352
353   /**
354    * closes the datasource and tidies up. source will be left in an error state
355    */
356   public void close() throws IOException
357   {
358     errormessage = "EXCEPTION ON CLOSE";
359     error = true;
360     dataIn.close();
361     dataIn = null;
362     errormessage = "SOURCE IS CLOSED";
363   }
364
365   /**
366    * rewinds the datasource the beginning.
367    * 
368    */
369   public void reset() throws IOException
370   {
371     if (dataIn != null && !error)
372     {
373       dataIn.reset();
374     }
375     else
376     {
377       throw new IOException(
378               "Implementation Error: Reset called for invalid source.");
379     }
380   }
381
382   /**
383    * 
384    * @return true if there is a warning for the user
385    */
386   public boolean hasWarningMessage()
387   {
388     return (warningMessage != null && warningMessage.length() > 0);
389   }
390
391   /**
392    * 
393    * @return empty string or warning message about file that was just parsed.
394    */
395   public String getWarningMessage()
396   {
397     return warningMessage;
398   }
399
400   public String getInFile()
401   {
402     if (inFile != null)
403     {
404       return inFile.getAbsolutePath() + " (" + index + ")";
405     }
406     else
407     {
408       return "From Paste + (" + index + ")";
409     }
410   }
411
412   /**
413    * @return the dataName
414    */
415   public String getDataName()
416   {
417     return dataName;
418   }
419
420   /**
421    * set the (human readable) name or URI for this datasource
422    * 
423    * @param dataname
424    */
425   protected void setDataName(String dataname)
426   {
427     dataName = dataname;
428   }
429
430   /**
431    * get the underlying bufferedReader for this data source.
432    * 
433    * @return null if no reader available
434    * @throws IOException
435    */
436   public Reader getReader()
437   {
438     if (dataIn != null) // Probably don't need to test for readiness &&
439                         // dataIn.ready())
440     {
441       return dataIn;
442     }
443     return null;
444   }
445 }