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