Merge branch 'develop' into features/JAL-1705_ensembl
[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    * not for general use, creates a fileParse object for an existing reader with
278    * configurable values for the origin and the type of the source
279    */
280   public FileParse(BufferedReader source, String originString,
281           String typeString)
282   {
283     type = typeString;
284     error = false;
285     inFile = null;
286     dataName = originString;
287     dataIn = source;
288     try
289     {
290       if (dataIn.markSupported())
291       {
292         dataIn.mark(READAHEAD_LIMIT);
293       }
294     } catch (IOException q)
295     {
296
297     }
298   }
299
300   /**
301    * Create a datasource for input to Jalview. See AppletFormatAdapter for the
302    * types of sources that are handled.
303    * 
304    * @param fileStr
305    *          - datasource locator/content
306    * @param type
307    *          - protocol of source
308    * @throws MalformedURLException
309    * @throws IOException
310    */
311   public FileParse(String fileStr, String type)
312           throws MalformedURLException, IOException
313   {
314     this.type = type;
315     error = false;
316
317     if (type.equals(AppletFormatAdapter.FILE))
318     {
319       if (checkFileSource(fileStr))
320       {
321         String suffixLess = extractSuffix(fileStr);
322         if (suffixLess != null)
323         {
324           if (checkFileSource(suffixLess))
325           {
326             throw new IOException(MessageManager.formatMessage(
327                     "exception.problem_opening_file_also_tried",
328                     new String[] { inFile.getName(), suffixLess,
329                         errormessage }));
330           }
331         }
332         else
333         {
334           throw new IOException(MessageManager.formatMessage(
335                   "exception.problem_opening_file",
336                   new String[] { inFile.getName(), errormessage }));
337         }
338       }
339     }
340     else if (type.equals(AppletFormatAdapter.URL))
341     {
342       try
343       {
344         try
345         {
346           checkURLSource(fileStr);
347           if (suffixSeparator == '#')
348           {
349             extractSuffix(fileStr); // URL lref is stored for later reference.
350           }
351         } catch (IOException e)
352         {
353           String suffixLess = extractSuffix(fileStr);
354           if (suffixLess == null)
355           {
356             throw (e);
357           }
358           else
359           {
360             try
361             {
362               checkURLSource(suffixLess);
363             } catch (IOException e2)
364             {
365               errormessage = "BAD URL WITH OR WITHOUT SUFFIX";
366               throw (e); // just pass back original - everything was wrong.
367             }
368           }
369         }
370       } catch (Exception e)
371       {
372         errormessage = "CANNOT ACCESS DATA AT URL '" + fileStr + "' ("
373                 + e.getMessage() + ")";
374         error = true;
375       }
376     }
377     else if (type.equals(AppletFormatAdapter.PASTE))
378     {
379       errormessage = "PASTE INACCESSIBLE!";
380       dataIn = new BufferedReader(new StringReader(fileStr));
381       dataName = "Paste";
382     }
383     else if (type.equals(AppletFormatAdapter.CLASSLOADER))
384     {
385       errormessage = "RESOURCE CANNOT BE LOCATED";
386       java.io.InputStream is = getClass()
387               .getResourceAsStream("/" + fileStr);
388       if (is == null)
389       {
390         String suffixLess = extractSuffix(fileStr);
391         if (suffixLess != null)
392         {
393           is = getClass().getResourceAsStream("/" + suffixLess);
394         }
395       }
396       if (is != null)
397       {
398         dataIn = new BufferedReader(new java.io.InputStreamReader(is));
399         dataName = fileStr;
400       }
401       else
402       {
403         error = true;
404       }
405     }
406     else
407     {
408       errormessage = "PROBABLE IMPLEMENTATION ERROR : Datasource Type given as '"
409               + (type != null ? type : "null") + "'";
410       error = true;
411     }
412     if (dataIn == null || error)
413     {
414       // pass up the reason why we have no source to read from
415       throw new IOException(MessageManager.formatMessage(
416               "exception.failed_to_read_data_from_source",
417               new String[] { errormessage }));
418     }
419     error = false;
420     dataIn.mark(READAHEAD_LIMIT);
421   }
422
423   /**
424    * mark the current position in the source as start for the purposes of it
425    * being analysed by IdentifyFile().identify
426    * 
427    * @throws IOException
428    */
429   public void mark() throws IOException
430   {
431     if (dataIn != null)
432     {
433       dataIn.mark(READAHEAD_LIMIT);
434     }
435     else
436     {
437       throw new IOException(
438               MessageManager.getString("exception.no_init_source_stream"));
439     }
440   }
441
442   public String nextLine() throws IOException
443   {
444     if (!error)
445     {
446       return dataIn.readLine();
447     }
448     throw new IOException(MessageManager.formatMessage(
449             "exception.invalid_source_stream",
450             new String[] { errormessage }));
451   }
452
453   /**
454    * 
455    * @return true if this FileParse is configured for Export only
456    */
457   public boolean isExporting()
458   {
459     return !error && dataIn == null;
460   }
461
462   /**
463    * 
464    * @return true if the data source is valid
465    */
466   public boolean isValid()
467   {
468     return !error;
469   }
470
471   /**
472    * closes the datasource and tidies up. source will be left in an error state
473    */
474   public void close() throws IOException
475   {
476     errormessage = "EXCEPTION ON CLOSE";
477     error = true;
478     dataIn.close();
479     dataIn = null;
480     errormessage = "SOURCE IS CLOSED";
481   }
482
483   /**
484    * rewinds the datasource the beginning.
485    * 
486    */
487   public void reset() throws IOException
488   {
489     if (dataIn != null && !error)
490     {
491       dataIn.reset();
492     }
493     else
494     {
495       throw new IOException(
496               MessageManager
497                       .getString("error.implementation_error_reset_called_for_invalid_source"));
498     }
499   }
500
501   /**
502    * 
503    * @return true if there is a warning for the user
504    */
505   public boolean hasWarningMessage()
506   {
507     return (warningMessage != null && warningMessage.length() > 0);
508   }
509
510   /**
511    * 
512    * @return empty string or warning message about file that was just parsed.
513    */
514   public String getWarningMessage()
515   {
516     return warningMessage;
517   }
518
519   public String getInFile()
520   {
521     if (inFile != null)
522     {
523       return inFile.getAbsolutePath() + " (" + index + ")";
524     }
525     else
526     {
527       return "From Paste + (" + index + ")";
528     }
529   }
530
531   /**
532    * @return the dataName
533    */
534   public String getDataName()
535   {
536     return dataName;
537   }
538
539   /**
540    * set the (human readable) name or URI for this datasource
541    * 
542    * @param dataname
543    */
544   protected void setDataName(String dataname)
545   {
546     dataName = dataname;
547   }
548
549   /**
550    * get the underlying bufferedReader for this data source.
551    * 
552    * @return null if no reader available
553    * @throws IOException
554    */
555   public Reader getReader()
556   {
557     if (dataIn != null) // Probably don't need to test for readiness &&
558                         // dataIn.ready())
559     {
560       return dataIn;
561     }
562     return null;
563   }
564
565   public AlignViewportI getViewport()
566   {
567     return viewport;
568   }
569
570   public void setViewport(AlignViewportI viewport)
571   {
572     this.viewport = viewport;
573   }
574
575   /**
576    * @return the currently configured exportSettings for writing data.
577    */
578   public AlignExportSettingI getExportSettings()
579   {
580     return exportSettings;
581   }
582
583   /**
584    * Set configuration for export of data.
585    * 
586    * @param exportSettings
587    *          the exportSettings to set
588    */
589   public void setExportSettings(AlignExportSettingI exportSettings)
590   {
591     this.exportSettings = exportSettings;
592   }
593
594   /**
595    * method overridden by complex file exporter/importers which support
596    * exporting visualisation and layout settings for a view
597    * 
598    * @param avpanel
599    */
600   public void configureForView(AlignmentViewPanel avpanel)
601   {
602     if (avpanel != null)
603     {
604       setViewport(avpanel.getAlignViewport());
605     }
606     // could also set export/import settings
607   }
608 }