Merge branch 'develop' into features/JAL-2909_bamImport2_11
[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.api.FeatureSettingsModelI;
27 import jalview.util.MessageManager;
28
29 import java.io.BufferedReader;
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.FileReader;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.InputStreamReader;
36 import java.io.Reader;
37 import java.io.StringReader;
38 import java.net.MalformedURLException;
39 import java.net.URL;
40 import java.util.zip.GZIPInputStream;
41
42 /**
43  * implements a random access wrapper around a particular datasource, for
44  * passing to identifyFile and AlignFile objects.
45  */
46 public class FileParse
47 {
48   /**
49    * text specifying source of data. usually filename or url.
50    */
51   private String dataName = "unknown source";
52
53   public File inFile = null;
54
55   /**
56    * a viewport associated with the current file operation. May be null. May
57    * move to different object.
58    */
59   private AlignViewportI viewport;
60
61   /**
62    * specific settings for exporting data from the current context
63    */
64   private AlignExportSettingI exportSettings;
65
66   /**
67    * sequence counter for FileParse object created from same data source
68    */
69   public int index = 1;
70
71   /**
72    * separator for extracting specific 'frame' of a datasource for formats that
73    * support multiple records (e.g. BLC, Stockholm, etc)
74    */
75   protected char suffixSeparator = '#';
76
77   /**
78    * character used to write newlines
79    */
80   protected String newline = System.getProperty("line.separator");
81
82   public void setNewlineString(String nl)
83   {
84     newline = nl;
85   }
86
87   public String getNewlineString()
88   {
89     return newline;
90   }
91
92   /**
93    * '#' separated string tagged on to end of filename or url that was clipped
94    * off to resolve to valid filename
95    */
96   protected String suffix = null;
97
98   protected DataSourceType dataSourceType = null;
99
100   protected BufferedReader dataIn = null;
101
102   protected String errormessage = "UNINITIALISED SOURCE";
103
104   protected boolean error = true;
105
106   protected String warningMessage = null;
107
108   /**
109    * size of readahead buffer used for when initial stream position is marked.
110    */
111   final int READAHEAD_LIMIT = 2048;
112
113   public FileParse()
114   {
115   }
116
117   /**
118    * Create a new FileParse instance reading from the same datasource starting
119    * at the current position. WARNING! Subsequent reads from either object will
120    * affect the read position of the other, but not the error state.
121    * 
122    * @param from
123    */
124   public FileParse(FileParse from) throws IOException
125   {
126     if (from == null)
127     {
128       throw new Error(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     dataSourceType = from.dataSourceType;
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(
207             new InputStreamReader(new GZIPInputStream(inputStream)));
208     inData.mark(2048);
209     inData.read();
210     inData.reset();
211     return inData;
212   }
213
214   private boolean checkURLSource(String fileStr)
215           throws IOException, 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(MessageManager
245                 .getString("exception.failed_to_resolve_gzip_stream"), e);
246       }
247       throw q;
248     }
249     // record URL as name of datasource.
250     dataName = fileStr;
251     return false;
252   }
253
254   /**
255    * sets the suffix string (if any) and returns remainder (if suffix was
256    * detected)
257    * 
258    * @param fileStr
259    * @return truncated fileStr or null
260    */
261   private String extractSuffix(String fileStr)
262   {
263     // first check that there wasn't a suffix string tagged on.
264     int sfpos = fileStr.lastIndexOf(suffixSeparator);
265     if (sfpos > -1 && sfpos < fileStr.length() - 1)
266     {
267       suffix = fileStr.substring(sfpos + 1);
268       // System.err.println("DEBUG: Found Suffix:"+suffix);
269       return fileStr.substring(0, sfpos);
270     }
271     return null;
272   }
273
274   /**
275    * not for general use, creates a fileParse object for an existing reader with
276    * configurable values for the origin and the type of the source
277    */
278   public FileParse(BufferedReader source, String originString,
279           DataSourceType sourceType)
280   {
281     dataSourceType = sourceType;
282     error = false;
283     inFile = null;
284     dataName = originString;
285     dataIn = source;
286     try
287     {
288       if (dataIn.markSupported())
289       {
290         dataIn.mark(READAHEAD_LIMIT);
291       }
292     } catch (IOException q)
293     {
294
295     }
296   }
297
298   /**
299    * Create a datasource for input to Jalview. See AppletFormatAdapter for the
300    * types of sources that are handled.
301    * 
302    * @param fileStr
303    *          - datasource locator/content
304    * @param sourceType
305    *          - protocol of source
306    * @throws MalformedURLException
307    * @throws IOException
308    */
309   public FileParse(String fileStr, DataSourceType sourceType)
310           throws MalformedURLException, IOException
311   {
312     this.dataSourceType = sourceType;
313     error = false;
314
315     if (sourceType == DataSourceType.FILE)
316     {
317       if (checkFileSource(fileStr))
318       {
319         String suffixLess = extractSuffix(fileStr);
320         if (suffixLess != null)
321         {
322           if (checkFileSource(suffixLess))
323           {
324             throw new IOException(MessageManager.formatMessage(
325                     "exception.problem_opening_file_also_tried",
326                     new String[]
327                     { inFile.getName(), suffixLess, errormessage }));
328           }
329         }
330         else
331         {
332           throw new IOException(MessageManager.formatMessage(
333                   "exception.problem_opening_file", new String[]
334                   { inFile.getName(), errormessage }));
335         }
336       }
337     }
338     else if (sourceType == DataSourceType.URL)
339     {
340       try
341       {
342         try
343         {
344           checkURLSource(fileStr);
345           if (suffixSeparator == '#')
346           {
347             dataName = extractSuffix(fileStr); // URL lref is stored for later
348                                                // reference.
349           }
350         } catch (IOException e)
351         {
352           String suffixLess = extractSuffix(fileStr);
353           if (suffixLess == null)
354           {
355             throw (e);
356           }
357           else
358           {
359             try
360             {
361               checkURLSource(suffixLess);
362             } catch (IOException e2)
363             {
364               errormessage = "BAD URL WITH OR WITHOUT SUFFIX";
365               throw (e); // just pass back original - everything was wrong.
366             }
367           }
368         }
369       } catch (Exception e)
370       {
371         errormessage = "CANNOT ACCESS DATA AT URL '" + fileStr + "' ("
372                 + e.getMessage() + ")";
373         error = true;
374       }
375     }
376     else if (sourceType == DataSourceType.PASTE)
377     {
378       errormessage = "PASTE INACCESSIBLE!";
379       dataIn = new BufferedReader(new StringReader(fileStr));
380       dataName = "Paste";
381     }
382     else if (sourceType == DataSourceType.CLASSLOADER)
383     {
384       errormessage = "RESOURCE CANNOT BE LOCATED";
385       java.io.InputStream is = getClass()
386               .getResourceAsStream("/" + fileStr);
387       if (is == null)
388       {
389         String suffixLess = extractSuffix(fileStr);
390         if (suffixLess != null)
391         {
392           is = getClass().getResourceAsStream("/" + suffixLess);
393         }
394       }
395       if (is != null)
396       {
397         dataIn = new BufferedReader(new java.io.InputStreamReader(is));
398         dataName = fileStr;
399       }
400       else
401       {
402         error = true;
403       }
404     }
405     else
406     {
407       errormessage = "PROBABLE IMPLEMENTATION ERROR : Datasource Type given as '"
408               + (sourceType != null ? sourceType : "null") + "'";
409       error = true;
410     }
411     if (dataIn == null || error)
412     {
413       // pass up the reason why we have no source to read from
414       throw new IOException(MessageManager.formatMessage(
415               "exception.failed_to_read_data_from_source", new String[]
416               { errormessage }));
417     }
418     error = false;
419     dataIn.mark(READAHEAD_LIMIT);
420   }
421
422   /**
423    * mark the current position in the source as start for the purposes of it
424    * being analysed by IdentifyFile().identify
425    * 
426    * @throws IOException
427    */
428   public void mark() throws IOException
429   {
430     if (dataIn != null)
431     {
432       dataIn.mark(READAHEAD_LIMIT);
433     }
434     else
435     {
436       throw new IOException(
437               MessageManager.getString("exception.no_init_source_stream"));
438     }
439   }
440
441   public String nextLine() throws IOException
442   {
443     if (!error)
444     {
445       return dataIn.readLine();
446     }
447     throw new IOException(MessageManager
448             .formatMessage("exception.invalid_source_stream", new String[]
449             { errormessage }));
450   }
451
452   /**
453    * 
454    * @return true if this FileParse is configured for Export only
455    */
456   public boolean isExporting()
457   {
458     return !error && dataIn == null;
459   }
460
461   /**
462    * 
463    * @return true if the data source is valid
464    */
465   public boolean isValid()
466   {
467     return !error;
468   }
469
470   /**
471    * closes the datasource and tidies up. source will be left in an error state
472    */
473   public void close() throws IOException
474   {
475     errormessage = "EXCEPTION ON CLOSE";
476     error = true;
477     dataIn.close();
478     dataIn = null;
479     errormessage = "SOURCE IS CLOSED";
480   }
481
482   /**
483    * Rewinds the datasource to the marked point if possible
484    * 
485    * @param bytesRead
486    * 
487    */
488   public void reset(int bytesRead) throws IOException
489   {
490     if (bytesRead >= READAHEAD_LIMIT)
491     {
492       System.err.println(String.format(
493               "File reset error: read %d bytes but reset limit is %d",
494               bytesRead, READAHEAD_LIMIT));
495     }
496     if (dataIn != null && !error)
497     {
498       dataIn.reset();
499     }
500     else
501     {
502       throw new IOException(MessageManager.getString(
503               "error.implementation_error_reset_called_for_invalid_source"));
504     }
505   }
506
507   /**
508    * 
509    * @return true if there is a warning for the user
510    */
511   public boolean hasWarningMessage()
512   {
513     return (warningMessage != null && warningMessage.length() > 0);
514   }
515
516   /**
517    * 
518    * @return empty string or warning message about file that was just parsed.
519    */
520   public String getWarningMessage()
521   {
522     return warningMessage;
523   }
524
525   public String getInFile()
526   {
527     if (inFile != null)
528     {
529       return inFile.getAbsolutePath() + " (" + index + ")";
530     }
531     else
532     {
533       return "From Paste + (" + index + ")";
534     }
535   }
536
537   /**
538    * @return the dataName
539    */
540   public String getDataName()
541   {
542     return dataName;
543   }
544
545   /**
546    * set the (human readable) name or URI for this datasource
547    * 
548    * @param dataname
549    */
550   protected void setDataName(String dataname)
551   {
552     dataName = dataname;
553   }
554
555   /**
556    * get the underlying bufferedReader for this data source.
557    * 
558    * @return null if no reader available
559    * @throws IOException
560    */
561   public Reader getReader()
562   {
563     if (dataIn != null) // Probably don't need to test for readiness &&
564                         // dataIn.ready())
565     {
566       return dataIn;
567     }
568     return null;
569   }
570
571   public AlignViewportI getViewport()
572   {
573     return viewport;
574   }
575
576   public void setViewport(AlignViewportI viewport)
577   {
578     this.viewport = viewport;
579   }
580
581   /**
582    * @return the currently configured exportSettings for writing data.
583    */
584   public AlignExportSettingI getExportSettings()
585   {
586     return exportSettings;
587   }
588
589   /**
590    * Set configuration for export of data.
591    * 
592    * @param exportSettings
593    *          the exportSettings to set
594    */
595   public void setExportSettings(AlignExportSettingI exportSettings)
596   {
597     this.exportSettings = exportSettings;
598   }
599
600   /**
601    * method overridden by complex file exporter/importers which support
602    * exporting visualisation and layout settings for a view
603    * 
604    * @param avpanel
605    */
606   public void configureForView(AlignmentViewPanel avpanel)
607   {
608     if (avpanel != null)
609     {
610       setViewport(avpanel.getAlignViewport());
611     }
612     // could also set export/import settings
613   }
614
615   /**
616    * Returns the preferred feature colour configuration if there is one, else
617    * null
618    * 
619    * @return
620    */
621   public FeatureSettingsModelI getFeatureColourScheme()
622   {
623     return null;
624   }
625
626   public DataSourceType getDataSourceType()
627   {
628     return dataSourceType;
629   }
630 }