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