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