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