JAL-1705 - allow a FileParse to be constructed directly from a reader for direct...
[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    * not for general use, creates a fileParse object for an existing reader with
273    * configurable values for the origin and the type of the source
274    */
275   public FileParse(BufferedReader source, String originString,
276           String typeString)
277   {
278     type = typeString;
279     error = false;
280     inFile = null;
281     dataName = originString;
282     dataIn = source;
283     try
284     {
285       if (dataIn.markSupported())
286       {
287         dataIn.mark(READAHEAD_LIMIT);
288       }
289     } catch (IOException q)
290     {
291
292     }
293   }
294
295   /**
296    * Create a datasource for input to Jalview. See AppletFormatAdapter for the
297    * types of sources that are handled.
298    * 
299    * @param fileStr
300    *          - datasource locator/content
301    * @param type
302    *          - protocol of source
303    * @throws MalformedURLException
304    * @throws IOException
305    */
306   public FileParse(String fileStr, String type)
307           throws MalformedURLException, IOException
308   {
309     this.type = type;
310     error = false;
311
312     if (type.equals(AppletFormatAdapter.FILE))
313     {
314       if (checkFileSource(fileStr))
315       {
316         String suffixLess = extractSuffix(fileStr);
317         if (suffixLess != null)
318         {
319           if (checkFileSource(suffixLess))
320           {
321             throw new IOException(MessageManager.formatMessage("exception.problem_opening_file_also_tried", new String[]{inFile.getName(),suffixLess,errormessage}));
322           }
323         }
324         else
325         {
326           throw new IOException(MessageManager.formatMessage("exception.problem_opening_file", new String[]{inFile.getName(),errormessage}));
327         }
328       }
329     }
330     else if (type.equals(AppletFormatAdapter.URL))
331     {
332       try
333       {
334         try
335         {
336           checkURLSource(fileStr);
337           if (suffixSeparator == '#')
338            {
339             extractSuffix(fileStr); // URL lref is stored for later reference.
340           }
341         } catch (IOException e)
342         {
343           String suffixLess = extractSuffix(fileStr);
344           if (suffixLess == null)
345           {
346             throw (e);
347           }
348           else
349           {
350             try
351             {
352               checkURLSource(suffixLess);
353             } catch (IOException e2)
354             {
355               errormessage = "BAD URL WITH OR WITHOUT SUFFIX";
356               throw (e); // just pass back original - everything was wrong.
357             }
358           }
359         }
360       } catch (Exception e)
361       {
362         errormessage = "CANNOT ACCESS DATA AT URL '" + fileStr + "' ("
363                 + e.getMessage() + ")";
364         error = true;
365       }
366     }
367     else if (type.equals(AppletFormatAdapter.PASTE))
368     {
369       errormessage = "PASTE INACCESSIBLE!";
370       dataIn = new BufferedReader(new StringReader(fileStr));
371       dataName = "Paste";
372     }
373     else if (type.equals(AppletFormatAdapter.CLASSLOADER))
374     {
375       errormessage = "RESOURCE CANNOT BE LOCATED";
376       java.io.InputStream is = getClass()
377               .getResourceAsStream("/" + fileStr);
378       if (is == null)
379       {
380         String suffixLess = extractSuffix(fileStr);
381         if (suffixLess != null)
382         {
383           is = getClass().getResourceAsStream("/" + suffixLess);
384         }
385       }
386       if (is != null)
387       {
388         dataIn = new BufferedReader(new java.io.InputStreamReader(is));
389         dataName = fileStr;
390       }
391       else
392       {
393         error = true;
394       }
395     }
396     else
397     {
398       errormessage = "PROBABLE IMPLEMENTATION ERROR : Datasource Type given as '"
399               + (type != null ? type : "null") + "'";
400       error = true;
401     }
402     if (dataIn == null || error)
403     {
404       // pass up the reason why we have no source to read from
405       throw new IOException(MessageManager.formatMessage("exception.failed_to_read_data_from_source", new String[]{errormessage}));
406     }
407     error = false;
408     dataIn.mark(READAHEAD_LIMIT);
409   }
410
411   /**
412    * mark the current position in the source as start for the purposes of it
413    * being analysed by IdentifyFile().identify
414    * 
415    * @throws IOException
416    */
417   public void mark() throws IOException
418   {
419     if (dataIn != null)
420     {
421       dataIn.mark(READAHEAD_LIMIT);
422     }
423     else
424     {
425       throw new IOException(MessageManager.getString("exception.no_init_source_stream"));
426     }
427   }
428
429   public String nextLine() throws IOException
430   {
431     if (!error)
432     {
433       return dataIn.readLine();
434     }
435     throw new IOException(MessageManager.formatMessage("exception.invalid_source_stream", new String[]{errormessage}));
436   }
437
438   /**
439    * 
440    * @return true if this FileParse is configured for Export only
441    */
442   public boolean isExporting()
443   {
444     return !error && dataIn == null;
445   }
446
447   /**
448    * 
449    * @return true if the data source is valid
450    */
451   public boolean isValid()
452   {
453     return !error;
454   }
455
456   /**
457    * closes the datasource and tidies up. source will be left in an error state
458    */
459   public void close() throws IOException
460   {
461     errormessage = "EXCEPTION ON CLOSE";
462     error = true;
463     dataIn.close();
464     dataIn = null;
465     errormessage = "SOURCE IS CLOSED";
466   }
467
468   /**
469    * rewinds the datasource the beginning.
470    * 
471    */
472   public void reset() throws IOException
473   {
474     if (dataIn != null && !error)
475     {
476       dataIn.reset();
477     }
478     else
479     {
480       throw new IOException(MessageManager.getString("error.implementation_error_reset_called_for_invalid_source"));
481     }
482   }
483
484   /**
485    * 
486    * @return true if there is a warning for the user
487    */
488   public boolean hasWarningMessage()
489   {
490     return (warningMessage != null && warningMessage.length() > 0);
491   }
492
493   /**
494    * 
495    * @return empty string or warning message about file that was just parsed.
496    */
497   public String getWarningMessage()
498   {
499     return warningMessage;
500   }
501
502   public String getInFile()
503   {
504     if (inFile != null)
505     {
506       return inFile.getAbsolutePath() + " (" + index + ")";
507     }
508     else
509     {
510       return "From Paste + (" + index + ")";
511     }
512   }
513
514   /**
515    * @return the dataName
516    */
517   public String getDataName()
518   {
519     return dataName;
520   }
521
522   /**
523    * set the (human readable) name or URI for this datasource
524    * 
525    * @param dataname
526    */
527   protected void setDataName(String dataname)
528   {
529     dataName = dataname;
530   }
531
532   /**
533    * get the underlying bufferedReader for this data source.
534    * 
535    * @return null if no reader available
536    * @throws IOException
537    */
538   public Reader getReader()
539   {
540     if (dataIn != null) // Probably don't need to test for readiness &&
541                         // dataIn.ready())
542     {
543       return dataIn;
544     }
545     return null;
546   }
547
548   public AlignViewportI getViewport()
549   {
550     return viewport;
551   }
552
553   public void setViewport(AlignViewportI viewport)
554   {
555     this.viewport = viewport;
556   }
557
558   /**
559    * @return the currently configured exportSettings for writing data.
560    */
561   public AlignExportSettingI getExportSettings()
562   {
563     return exportSettings;
564   }
565
566   /**
567    * Set configuration for export of data.
568    * 
569    * @param exportSettings
570    *          the exportSettings to set
571    */
572   public void setExportSettings(AlignExportSettingI exportSettings)
573   {
574     this.exportSettings = exportSettings;
575   }
576
577   /**
578    * method overridden by complex file exporter/importers which support
579    * exporting visualisation and layout settings for a view
580    * 
581    * @param avpanel
582    */
583   public void configureForView(AlignmentViewPanel avpanel)
584   {
585     if (avpanel!=null) {
586       setViewport(avpanel.getAlignViewport());
587     }
588     // could also set export/import settings
589   }
590 }