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