8251c2276a19243703f1b3ad2095ae4b924243ee
[jalview.git] / src / jalview / io / IdentifyFile.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 java.io.File;
24 import java.io.FileNotFoundException;
25 import java.io.IOException;
26 import java.util.Locale;
27
28 import jalview.bin.Console;
29
30 /**
31  * DOCUMENT ME!
32  *
33  * @author $author$
34  * @version $Revision$
35  */
36 public class IdentifyFile
37 {
38
39   private static final String XMLHEADER = "<?XML VERSION=\"1.0\" ENCODING=\"UTF-8\" STANDALONE=\"YES\"?>";
40
41   public FileFormatI identify(Object file, DataSourceType protocol)
42           throws FileFormatException, FileNotFoundException
43   {
44     // BH 2018
45     return (file instanceof File ? identify((File) file, protocol)
46             : identify((String) file, protocol));
47
48   }
49
50   public FileFormatI identify(File file, DataSourceType sourceType)
51           throws FileFormatException
52   {
53     // BH 2018
54     String emessage = "UNIDENTIFIED FILE PARSING ERROR";
55     FileParse parser = null;
56     try
57     {
58       parser = new FileParse(file, sourceType);
59       if (parser.isValid())
60       {
61         return identify(parser);
62       }
63     } catch (Exception e)
64     {
65       Console.error("Error whilst identifying " + file, e);
66       emessage = e.getMessage();
67     }
68     if (parser != null)
69     {
70       throw new FileFormatException(parser.errormessage);
71     }
72     throw new FileFormatException(emessage);
73   }
74
75   /**
76    * Identify a datasource's file content.
77    *
78    * @note Do not use this method for stream sources - create a FileParse object
79    *       instead.
80    *
81    * @param file
82    * @param sourceType
83    * @return
84    * @throws FileFormatException
85    */
86   public FileFormatI identify(String file, DataSourceType sourceType)
87           throws FileFormatException, FileNotFoundException
88   {
89     String emessage = "UNIDENTIFIED FILE PARSING ERROR";
90     FileParse parser = null;
91     FileNotFoundException fnf = null;
92     try
93     {
94       parser = new FileParse(file, sourceType);
95       if (parser.isValid())
96       {
97         return identify(parser);
98       }
99     } catch (FileNotFoundException e)
100     {
101       fnf = e;
102       emessage = "Could not find '" + file + "'";
103       Console.error("Could not find '" + file + "'", e);
104     } catch (IOException e)
105     {
106       Console.error("Error whilst trying to read " + file, e);
107     } catch (Exception e)
108     {
109       Console.error("Error whilst identifying " + file, e);
110       emessage = e.getMessage();
111     }
112     if (parser != null)
113     {
114       throw new FileFormatException(parser.errormessage);
115     }
116     if (fnf != null)
117     {
118       throw (fnf);
119     }
120     throw new FileFormatException(emessage);
121   }
122
123   public FileFormatI identify(FileParse source) throws FileFormatException
124   {
125     return identify(source, true);
126     // preserves original behaviour prior to version 2.3
127   }
128
129   public FileFormatI identify(AlignmentFileReaderI file,
130           boolean closeSource) throws IOException
131   {
132     FileParse fp = new FileParse(file.getInFile(),
133             file.getDataSourceType());
134     return identify(fp, closeSource);
135   }
136
137   /**
138    * Identify contents of source, closing it or resetting source to start
139    * afterwards.
140    *
141    * @param source
142    * @param closeSource
143    * @return (best guess at) file format
144    * @throws FileFormatException
145    */
146   public FileFormatI identify(FileParse source, boolean closeSource)
147           throws FileFormatException
148   {
149     FileFormatI reply = FileFormat.Pfam;
150     String data;
151     int bytesRead = 0;
152     int trimmedLength = 0;
153     boolean isXml = false; // set true if first line is XMLHEADER
154     boolean lineswereskipped = false;
155     boolean isBinary = false; // true if length is non-zero and non-printable
156     // characters are encountered
157
158     try
159     {
160       if (!closeSource)
161       {
162         source.mark();
163       }
164       boolean aaIndexHeaderRead = false;
165
166       while ((data = source.nextLine()) != null)
167       {
168         bytesRead += data.length();
169         trimmedLength += data.trim().length();
170         if (!lineswereskipped)
171         {
172           for (int i = 0; !isBinary && i < data.length(); i++)
173           {
174             char c = data.charAt(i);
175             isBinary = (c < 32 && c != '\t' && c != '\n' && c != '\r'
176                     && c != 5 && c != 27); // nominal binary character filter
177             // excluding CR, LF, tab,DEL and ^E
178             // for certain blast ids
179           }
180         }
181         if (isBinary)
182         {
183           // jar files are special - since they contain all sorts of random
184           // characters.
185           if (source.inFile != null)
186           {
187             String fileStr = source.inFile.getName();
188             if (fileStr.contains(".jar") || fileStr.contains(".zip")
189                     || fileStr.contains(".jvp"))
190             {
191               // possibly a Jalview archive (but check further)
192               reply = FileFormat.Jalview;
193             }
194           }
195           if (!lineswereskipped && data.startsWith("PK"))
196           {
197             reply = FileFormat.Jalview; // archive
198             break;
199           }
200         }
201         data = data.toUpperCase(Locale.ROOT);
202
203         if (data.startsWith(ScoreMatrixFile.SCOREMATRIX))
204         {
205           reply = FileFormat.ScoreMatrix;
206           break;
207         }
208         if (data.startsWith(XMLHEADER) && !lineswereskipped)
209         {
210           isXml = true;
211         }
212         if (data.startsWith("LOCUS"))
213         {
214           reply = FileFormat.GenBank;
215           break;
216         }
217         if (data.startsWith("ID "))
218         {
219           if (data.substring(2).trim().split(";").length == 7)
220           {
221             reply = FileFormat.Embl;
222             break;
223           }
224         }
225         if (data.startsWith("H ") && !aaIndexHeaderRead)
226         {
227           aaIndexHeaderRead = true;
228         }
229         if (data.startsWith("D ") && aaIndexHeaderRead)
230         {
231           reply = FileFormat.ScoreMatrix;
232           break;
233         }
234         if (data.startsWith("##GFF-VERSION"))
235         {
236           // GFF - possibly embedded in a Jalview features file!
237           reply = FileFormat.Features;
238           break;
239         }
240         if (looksLikeFeatureData(data))
241         {
242           reply = FileFormat.Features;
243           break;
244         }
245         if (data.indexOf("# STOCKHOLM") > -1)
246         {
247           reply = FileFormat.Stockholm;
248           break;
249         }
250         if (data.indexOf("_ENTRY.ID") > -1
251                 || data.indexOf("_AUDIT_AUTHOR.NAME") > -1
252                 || data.indexOf("_ATOM_SITE.") > -1)
253         {
254           reply = FileFormat.MMCif;
255           break;
256         }
257         // if (data.indexOf(">") > -1)
258         if (data.startsWith(">"))
259         {
260           // FASTA, PIR file or BLC file
261           boolean checkPIR = false, starterm = false;
262           if ((data.indexOf(">P1;") > -1) || (data.indexOf(">DL;") > -1))
263           {
264             // watch for PIR file attributes
265             checkPIR = true;
266             reply = FileFormat.PIR;
267           }
268           // could also be BLC file, read next line to confirm
269           data = source.nextLine();
270
271           if (data.indexOf(">") > -1)
272           {
273             reply = FileFormat.BLC;
274           }
275           else
276           {
277             // Is this a single line BLC file?
278             String data1 = source.nextLine();
279             String data2 = source.nextLine();
280             int c1;
281             if (checkPIR)
282             {
283               starterm = (data1 != null && data1.indexOf("*") > -1)
284                       || (data2 != null && data2.indexOf("*") > -1);
285             }
286             if (data2 != null && (c1 = data.indexOf("*")) > -1)
287             {
288               if (c1 == 0 && c1 == data2.indexOf("*"))
289               {
290                 reply = FileFormat.BLC;
291               }
292               else
293               {
294                 reply = FileFormat.Fasta; // possibly a bad choice - may be
295                                           // recognised as
296                 // PIR
297               }
298               // otherwise can still possibly be a PIR file
299             }
300             else
301             {
302               reply = FileFormat.Fasta;
303               // TODO : AMSA File is indicated if there is annotation in the
304               // FASTA file - but FASTA will automatically generate this at the
305               // mo.
306               if (!checkPIR)
307               {
308                 break;
309               }
310             }
311           }
312           // final check for PIR content. require
313           // >P1;title\n<blah>\nterminated sequence to occur at least once.
314
315           // TODO the PIR/fasta ambiguity may be the use case that is needed to
316           // have
317           // a 'Parse as type XXX' parameter for the applet/application.
318           if (checkPIR)
319           {
320             String dta = null;
321             if (!starterm)
322             {
323               do
324               {
325                 try
326                 {
327                   dta = source.nextLine();
328                 } catch (IOException ex)
329                 {
330                 }
331                 if (dta != null && dta.indexOf("*") > -1)
332                 {
333                   starterm = true;
334                 }
335               } while (dta != null && !starterm);
336             }
337             if (starterm)
338             {
339               reply = FileFormat.PIR;
340               break;
341             }
342             else
343             {
344               reply = FileFormat.Fasta; // probably a bad choice!
345             }
346           }
347           // read as a FASTA (probably)
348           break;
349         }
350         if (data.indexOf("{\"") > -1)
351         {
352           reply = FileFormat.Json;
353           break;
354         }
355         int lessThan = data.indexOf("<");
356         if ((lessThan > -1)) // possible Markup Language data i.e HTML,
357                              // RNAML, XML
358         {
359           String upper = data.toUpperCase(Locale.ROOT);
360           if (upper.substring(lessThan).startsWith("<HTML"))
361           {
362             reply = FileFormat.Html;
363             break;
364           }
365           if (upper.substring(lessThan).startsWith("<RNAML"))
366           {
367             reply = FileFormat.Rnaml;
368             break;
369           }
370           if (isXml && data.contains(
371                   "<NS2:JALVIEWUSERCOLOURS SCHEMENAME=\"SEQUENCE FEATURES\" XMLNS:NS2=\"WWW.JALVIEW.ORG/COLOURS\">"))
372           {
373             reply = FileFormat.FeatureSettings;
374             break;
375           }
376         }
377
378         if ((data.length() < 1) || (data.indexOf("#") == 0))
379         {
380           lineswereskipped = true;
381           continue;
382         }
383
384         if (data.indexOf("PILEUP") > -1)
385         {
386           reply = FileFormat.Pileup;
387
388           break;
389         }
390
391         if ((data.indexOf("//") == 0) || ((data.indexOf("!!") > -1) && (data
392                 .indexOf("!!") < data.indexOf("_MULTIPLE_ALIGNMENT "))))
393         {
394           reply = FileFormat.MSF;
395
396           break;
397         }
398         else if (data.indexOf("CLUSTAL") > -1)
399         {
400           reply = FileFormat.Clustal;
401
402           break;
403         }
404
405         else if (data.indexOf("HEADER") == 0 || data.indexOf("ATOM") == 0)
406         {
407           reply = FileFormat.PDB;
408           break;
409         }
410         else if (data.matches("\\s*\\d+\\s+\\d+\\s*"))
411         {
412           reply = FileFormat.Phylip;
413           break;
414         }
415         else
416         {
417           if (!lineswereskipped && looksLikeJnetData(data))
418           {
419             reply = FileFormat.Jnet;
420             break;
421           }
422         }
423
424         lineswereskipped = true; // this means there was some junk before any
425         // key file signature
426       }
427       if (closeSource)
428       {
429         source.close();
430       }
431       else
432       {
433         source.reset(bytesRead); // so the file can be parsed from the mark
434       }
435     } catch (Exception ex)
436     {
437       Console.error("File Identification failed!\n" + ex);
438       throw new FileFormatException(source.errormessage);
439     }
440     if (trimmedLength == 0)
441     {
442       Console.error("File Identification failed! - Empty file was read.");
443       throw new FileFormatException("EMPTY DATA FILE");
444     }
445     Console.debug("File format identified as " + reply.toString());
446     return reply;
447   }
448
449   /**
450    * Returns true if the data appears to be Jnet concise annotation format
451    * 
452    * @param data
453    * @return
454    */
455   protected boolean looksLikeJnetData(String data)
456   {
457     char firstChar = data.charAt(0);
458     int colonPos = data.indexOf(":");
459     int commaPos = data.indexOf(",");
460     boolean isJnet = firstChar != '*' && firstChar != ' ' && colonPos > -1
461             && commaPos > -1 && colonPos < commaPos;
462     // && data.indexOf(",")<data.indexOf(",", data.indexOf(","))) / ??
463     return isJnet;
464   }
465
466   /**
467    * Returns true if the data has at least 6 tab-delimited fields _and_ fields 4
468    * and 5 are integer (start/end)
469    * 
470    * @param data
471    * @return
472    */
473   protected boolean looksLikeFeatureData(String data)
474   {
475     if (data == null)
476     {
477       return false;
478     }
479     String[] columns = data.split("\t");
480     if (columns.length < 6)
481     {
482       return false;
483     }
484     for (int col = 3; col < 5; col++)
485     {
486       try
487       {
488         Integer.parseInt(columns[col]);
489       } catch (NumberFormatException e)
490       {
491         return false;
492       }
493     }
494     return true;
495   }
496
497   /**
498    * 
499    * @param args
500    * @j2sIgnore
501    */
502   public static void main(String[] args)
503   {
504     for (int i = 0; args != null && i < args.length; i++)
505     {
506       IdentifyFile ider = new IdentifyFile();
507       FileFormatI type = null;
508       try
509       {
510         type = ider.identify(args[i], DataSourceType.FILE);
511       } catch (FileNotFoundException e)
512       {
513         Console.error(String.format("Error '%s' fetching file %s", args[i],
514                 e.getMessage()));
515       } catch (FileFormatException e)
516       {
517         Console.error(
518                 String.format("Error '%s' identifying file type for %s",
519                         args[i], e.getMessage()));
520       }
521       Console.debug("Type of " + args[i] + " is " + type);
522     }
523     if (args == null || args.length == 0)
524     {
525       Console.error("Usage: <Filename> [<Filename> ...]");
526     }
527   }
528
529 }