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