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