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