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