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