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