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