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