JAL-2788 possible adjustments to sequence accesses
[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             extractSuffix(fileStr); // URL lref is stored for later reference.
348           }
349         } catch (IOException e)
350         {
351           String suffixLess = extractSuffix(fileStr);
352           if (suffixLess == null)
353           {
354             throw (e);
355           }
356           else
357           {
358             try
359             {
360               checkURLSource(suffixLess);
361             } catch (IOException e2)
362             {
363               errormessage = "BAD URL WITH OR WITHOUT SUFFIX";
364               throw (e); // just pass back original - everything was wrong.
365             }
366           }
367         }
368       } catch (Exception e)
369       {
370         errormessage = "CANNOT ACCESS DATA AT URL '" + fileStr + "' ("
371                 + e.getMessage() + ")";
372         error = true;
373       }
374     }
375     else if (sourceType == DataSourceType.PASTE)
376     {
377       errormessage = "PASTE INACCESSIBLE!";
378       dataIn = new BufferedReader(new StringReader(fileStr));
379       dataName = "Paste";
380     }
381     else if (sourceType == DataSourceType.CLASSLOADER)
382     {
383       errormessage = "RESOURCE CANNOT BE LOCATED";
384       java.io.InputStream is = getClass()
385               .getResourceAsStream("/" + fileStr);
386       if (is == null)
387       {
388         String suffixLess = extractSuffix(fileStr);
389         if (suffixLess != null)
390         {
391           is = getClass().getResourceAsStream("/" + suffixLess);
392         }
393       }
394       if (is != null)
395       {
396         dataIn = new BufferedReader(new java.io.InputStreamReader(is));
397         dataName = fileStr;
398       }
399       else
400       {
401         error = true;
402       }
403     }
404     else
405     {
406       errormessage = "PROBABLE IMPLEMENTATION ERROR : Datasource Type given as '"
407               + (sourceType != null ? sourceType : "null") + "'";
408       error = true;
409     }
410     if (dataIn == null || error)
411     {
412       // pass up the reason why we have no source to read from
413       throw new IOException(MessageManager.formatMessage(
414               "exception.failed_to_read_data_from_source", new String[]
415               { errormessage }));
416     }
417     error = false;
418     dataIn.mark(READAHEAD_LIMIT);
419   }
420
421   /**
422    * mark the current position in the source as start for the purposes of it
423    * being analysed by IdentifyFile().identify
424    * 
425    * @throws IOException
426    */
427   public void mark() throws IOException
428   {
429     if (dataIn != null)
430     {
431       dataIn.mark(READAHEAD_LIMIT);
432     }
433     else
434     {
435       throw new IOException(
436               MessageManager.getString("exception.no_init_source_stream"));
437     }
438   }
439
440   public String nextLine() throws IOException
441   {
442     if (!error)
443     {
444       return dataIn.readLine();
445     }
446     throw new IOException(MessageManager
447             .formatMessage("exception.invalid_source_stream", new String[]
448             { errormessage }));
449   }
450
451   /**
452    * 
453    * @return true if this FileParse is configured for Export only
454    */
455   public boolean isExporting()
456   {
457     return !error && dataIn == null;
458   }
459
460   /**
461    * 
462    * @return true if the data source is valid
463    */
464   public boolean isValid()
465   {
466     return !error;
467   }
468
469   /**
470    * closes the datasource and tidies up. source will be left in an error state
471    */
472   public void close() throws IOException
473   {
474     errormessage = "EXCEPTION ON CLOSE";
475     error = true;
476     dataIn.close();
477     dataIn = null;
478     errormessage = "SOURCE IS CLOSED";
479   }
480
481   /**
482    * Rewinds the datasource to the marked point if possible
483    * 
484    * @param bytesRead
485    * 
486    */
487   public void reset(int bytesRead) throws IOException
488   {
489     if (bytesRead >= READAHEAD_LIMIT)
490     {
491       System.err.println(String.format(
492               "File reset error: read %d bytes but reset limit is %d",
493               bytesRead, READAHEAD_LIMIT));
494     }
495     if (dataIn != null && !error)
496     {
497       dataIn.reset();
498     }
499     else
500     {
501       throw new IOException(MessageManager.getString(
502               "error.implementation_error_reset_called_for_invalid_source"));
503     }
504   }
505
506   /**
507    * 
508    * @return true if there is a warning for the user
509    */
510   public boolean hasWarningMessage()
511   {
512     return (warningMessage != null && warningMessage.length() > 0);
513   }
514
515   /**
516    * 
517    * @return empty string or warning message about file that was just parsed.
518    */
519   public String getWarningMessage()
520   {
521     return warningMessage;
522   }
523
524   public String getInFile()
525   {
526     if (inFile != null)
527     {
528       return inFile.getAbsolutePath() + " (" + index + ")";
529     }
530     else
531     {
532       return "From Paste + (" + index + ")";
533     }
534   }
535
536   /**
537    * @return the dataName
538    */
539   public String getDataName()
540   {
541     return dataName;
542   }
543
544   /**
545    * set the (human readable) name or URI for this datasource
546    * 
547    * @param dataname
548    */
549   protected void setDataName(String dataname)
550   {
551     dataName = dataname;
552   }
553
554   /**
555    * get the underlying bufferedReader for this data source.
556    * 
557    * @return null if no reader available
558    * @throws IOException
559    */
560   public Reader getReader()
561   {
562     if (dataIn != null) // Probably don't need to test for readiness &&
563                         // dataIn.ready())
564     {
565       return dataIn;
566     }
567     return null;
568   }
569
570   public AlignViewportI getViewport()
571   {
572     return viewport;
573   }
574
575   public void setViewport(AlignViewportI viewport)
576   {
577     this.viewport = viewport;
578   }
579
580   /**
581    * @return the currently configured exportSettings for writing data.
582    */
583   public AlignExportSettingI getExportSettings()
584   {
585     return exportSettings;
586   }
587
588   /**
589    * Set configuration for export of data.
590    * 
591    * @param exportSettings
592    *          the exportSettings to set
593    */
594   public void setExportSettings(AlignExportSettingI exportSettings)
595   {
596     this.exportSettings = exportSettings;
597   }
598
599   /**
600    * method overridden by complex file exporter/importers which support
601    * exporting visualisation and layout settings for a view
602    * 
603    * @param avpanel
604    */
605   public void configureForView(AlignmentViewPanel avpanel)
606   {
607     if (avpanel != null)
608     {
609       setViewport(avpanel.getAlignViewport());
610     }
611     // could also set export/import settings
612   }
613
614   /**
615    * Returns the preferred feature colour configuration if there is one, else
616    * null
617    * 
618    * @return
619    */
620   public FeatureSettingsModelI getFeatureColourScheme()
621   {
622     return null;
623   }
624
625   public DataSourceType getDataSourceType()
626   {
627     return dataSourceType;
628   }
629 }