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