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