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