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