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