Merge branch 'develop' into features/r2_11_2_alphafold/JAL-2349_JAL-3855
[jalview.git] / src / jalview / io / AppletFormatAdapter.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.util.Locale;
24
25 import jalview.api.AlignExportSettingsI;
26 import jalview.api.AlignmentViewPanel;
27 import jalview.datamodel.Alignment;
28 import jalview.datamodel.AlignmentAnnotation;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.AlignmentView;
31 import jalview.datamodel.SeqDistanceContactMatrix;
32 import jalview.datamodel.PDBEntry.Type;
33 import jalview.datamodel.SequenceI;
34 import jalview.ext.jmol.JmolParser;
35 import jalview.structure.StructureImportSettings;
36 import jalview.util.Platform;
37
38 import java.io.File;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.util.List;
42
43 /**
44  * A low level class for alignment and feature IO with alignment formatting
45  * methods used by both applet and application for generating flat alignment
46  * files. It also holds the lists of magic format names that the applet and
47  * application will allow the user to read or write files with.
48  *
49  * @author $author$
50  * @version $Revision$
51  */
52 public class AppletFormatAdapter
53 {
54   private AlignmentViewPanel viewpanel;
55
56   /**
57    * add jalview-derived non-secondary structure annotation from PDB structure
58    */
59   boolean annotFromStructure = false;
60
61   /**
62    * add secondary structure from PDB data with built-in algorithms
63    */
64   boolean localSecondaryStruct = false;
65
66   /**
67    * process PDB data with web services
68    */
69   boolean serviceSecondaryStruct = false;
70
71   private AlignmentFileReaderI alignFile = null;
72
73   String inFile;
74
75   /**
76    * character used to write newlines
77    */
78   protected String newline = System.getProperty("line.separator");
79
80   private AlignExportSettingsI exportSettings;
81
82   private File selectedFile;
83
84   public static String INVALID_CHARACTERS = "Contains invalid characters";
85
86   /**
87    * Returns an error message with a list of supported readable file formats
88    * 
89    * @return
90    */
91   public static String getSupportedFormats()
92   {
93     return "Formats currently supported are\n"
94             + prettyPrint(FileFormats.getInstance().getReadableFormats());
95   }
96
97   public AppletFormatAdapter()
98   {
99   }
100
101   public AppletFormatAdapter(AlignmentViewPanel viewpanel)
102   {
103     this.viewpanel = viewpanel;
104   }
105
106   public AppletFormatAdapter(AlignmentViewPanel alignPanel,
107           AlignExportSettingsI settings)
108   {
109     viewpanel = alignPanel;
110     exportSettings = settings;
111   }
112
113   /**
114    * Formats a grammatically correct(ish) list consisting of the given objects
115    * 
116    * @param things
117    * @return
118    */
119   public static String prettyPrint(List<? extends Object> things)
120   {
121     StringBuffer list = new StringBuffer();
122     for (int i = 0, iSize = things.size() - 1; i < iSize; i++)
123     {
124       list.append(things.get(i).toString());
125       list.append(", ");
126     }
127     // could i18n 'and' here
128     list.append(" and " + things.get(things.size() - 1).toString() + ".");
129     return list.toString();
130   }
131
132   public void setNewlineString(String nl)
133   {
134     newline = nl;
135   }
136
137   public String getNewlineString()
138   {
139     return newline;
140   }
141
142   /**
143    * Constructs the correct filetype parser for a characterised datasource
144    *
145    * @param inFile
146    *          data/data location
147    * @param sourceType
148    *          type of datasource
149    * @param fileFormat
150    *
151    * @return
152    */
153   public AlignmentI readFile(String file, DataSourceType sourceType,
154           FileFormatI fileFormat) throws IOException
155   {
156     return readFile(null, file, sourceType, fileFormat);
157   }
158   
159   public AlignmentI readFile(File selectedFile, String file, DataSourceType sourceType,
160           FileFormatI fileFormat) throws IOException
161   {
162
163     this.selectedFile = selectedFile;
164     if (selectedFile != null)
165     {
166       this.inFile = selectedFile.getPath();
167     }
168     this.inFile = file;
169     try
170     {
171       if (fileFormat.isStructureFile())
172       {
173         String structureParser = StructureImportSettings
174                 .getDefaultPDBFileParser();
175         boolean isParseWithJMOL = structureParser.equalsIgnoreCase(
176                 StructureImportSettings.StructureParser.JMOL_PARSER
177                         .toString());
178         StructureImportSettings.addSettings(annotFromStructure,
179                 localSecondaryStruct, serviceSecondaryStruct);
180         if (isParseWithJMOL)
181         {
182           // needs a File option
183           alignFile = new JmolParser(selectedFile == null ? inFile : selectedFile, sourceType);
184         }
185         else
186         {
187           // todo is mc_view parsing obsolete yet? JAL-2120
188           StructureImportSettings.setShowSeqFeatures(true);
189           alignFile = new mc_view.PDBfile(annotFromStructure,
190                   localSecondaryStruct, serviceSecondaryStruct, inFile,
191                   sourceType);
192         }
193         ((StructureFile) alignFile).setDbRefType(
194                 FileFormat.PDB.equals(fileFormat) ? Type.PDB : Type.MMCIF);
195       }
196       else if (selectedFile != null) {
197         alignFile = fileFormat.getReader(new FileParse(selectedFile, sourceType));
198       } else 
199       {
200         // alignFile = fileFormat.getAlignmentFile(inFile, sourceType);
201         alignFile = fileFormat.getReader(new FileParse(inFile, sourceType));
202       }
203       return buildAlignmentFromFile();
204     } catch (Exception e)
205     {
206       e.printStackTrace();
207       System.err.println("Failed to read alignment using the '" + fileFormat
208               + "' reader.\n" + e);
209
210       if (e.getMessage() != null
211               && e.getMessage().startsWith(INVALID_CHARACTERS))
212       {
213         throw new IOException(e.getMessage());
214       }
215
216       // Finally test if the user has pasted just the sequence, no id
217       if (sourceType == DataSourceType.PASTE)
218       {
219         try
220         {
221           // Possible sequence is just residues with no label
222           alignFile = new FastaFile(">UNKNOWN\n" + inFile,
223                   DataSourceType.PASTE);
224           return buildAlignmentFromFile();
225
226         } catch (Exception ex)
227         {
228           if (ex.toString().startsWith(INVALID_CHARACTERS))
229           {
230             throw new IOException(e.getMessage());
231           }
232
233           ex.printStackTrace();
234         }
235       }
236       if (FileFormat.Html.equals(fileFormat))
237       {
238         throw new IOException(e.getMessage());
239       }
240     }
241     throw new FileFormatException(getSupportedFormats());
242   }
243
244   /**
245    * Constructs the correct filetype parser for an already open datasource
246    *
247    * @param source
248    *          an existing datasource
249    * @param format
250    *          File format of data that will be provided by datasource
251    *
252    * @return
253    */
254   public AlignmentI readFromFile(FileParse source, FileFormatI format)
255           throws IOException
256   {
257     this.inFile = source.getInFile();
258     DataSourceType type = source.dataSourceType;
259     try
260     {
261       if (FileFormat.PDB.equals(format) || FileFormat.MMCif.equals(format))
262       {
263         // TODO obtain config value from preference settings
264         boolean isParseWithJMOL = false;
265         if (isParseWithJMOL)
266         {
267           StructureImportSettings.addSettings(annotFromStructure,
268                   localSecondaryStruct, serviceSecondaryStruct);
269           alignFile = new JmolParser(source);
270         }
271         else
272         {
273           StructureImportSettings.setShowSeqFeatures(true);
274           alignFile = new mc_view.PDBfile(annotFromStructure,
275                   localSecondaryStruct, serviceSecondaryStruct, source);
276         }
277         ((StructureFile) alignFile).setDbRefType(Type.PDB);
278       }
279       else
280       {
281         alignFile = format.getReader(source);
282       }
283
284       return buildAlignmentFromFile();
285
286     } catch (Exception e)
287     {
288       e.printStackTrace();
289       System.err.println("Failed to read alignment using the '" + format
290               + "' reader.\n" + e);
291
292       if (e.getMessage() != null
293               && e.getMessage().startsWith(INVALID_CHARACTERS))
294       {
295         throw new FileFormatException(e.getMessage());
296       }
297
298       // Finally test if the user has pasted just the sequence, no id
299       if (type == DataSourceType.PASTE)
300       {
301         try
302         {
303           // Possible sequence is just residues with no label
304           alignFile = new FastaFile(">UNKNOWN\n" + inFile,
305                   DataSourceType.PASTE);
306           return buildAlignmentFromFile();
307
308         } catch (Exception ex)
309         {
310           if (ex.toString().startsWith(INVALID_CHARACTERS))
311           {
312             throw new IOException(e.getMessage());
313           }
314
315           ex.printStackTrace();
316         }
317       }
318
319       // If we get to this stage, the format was not supported
320       throw new FileFormatException(getSupportedFormats());
321     }
322   }
323
324   /**
325    * boilerplate method to handle data from an AlignFile and construct a new
326    * alignment or import to an existing alignment
327    * 
328    * @return AlignmentI instance ready to pass to a UI constructor
329    */
330   private AlignmentI buildAlignmentFromFile()
331   {
332     // Standard boilerplate for creating alignment from parser
333     // alignFile.configureForView(viewpanel);
334
335     AlignmentI al = new Alignment(alignFile.getSeqsAsArray());
336
337     alignFile.addAnnotations(al);
338
339     alignFile.addGroups(al);
340
341     return al;
342   }
343
344   /**
345    * create an alignment flatfile from a Jalview alignment view
346    * 
347    * @param format
348    * @param jvsuffix
349    * @param av
350    * @param selectedOnly
351    * @return flatfile in a string
352    */
353   public String formatSequences(FileFormatI format, boolean jvsuffix,
354           AlignmentViewPanel ap, boolean selectedOnly)
355   {
356
357     AlignmentView selvew = ap.getAlignViewport()
358             .getAlignmentView(selectedOnly, false);
359     AlignmentI aselview = selvew
360             .getVisibleAlignment(ap.getAlignViewport().getGapCharacter());
361     List<AlignmentAnnotation> ala = (ap.getAlignViewport()
362             .getVisibleAlignmentAnnotation(selectedOnly));
363     if (ala != null)
364     {
365       for (AlignmentAnnotation aa : ala)
366       {
367         aselview.addAnnotation(aa);
368       }
369     }
370     viewpanel = ap;
371     return formatSequences(format, aselview, jvsuffix);
372   }
373
374   /**
375    * Construct an output class for an alignment in a particular filetype TODO:
376    * allow caller to detect errors and warnings encountered when generating
377    * output
378    *
379    * @param format
380    *          string name of alignment format
381    * @param alignment
382    *          the alignment to be written out
383    * @param jvsuffix
384    *          passed to AlnFile class controls whether /START-END is added to
385    *          sequence names
386    *
387    * @return alignment flat file contents
388    */
389   public String formatSequences(FileFormatI format, AlignmentI alignment,
390           boolean jvsuffix)
391   {
392     try
393     {
394       AlignmentFileWriterI afile = format.getWriter(alignment);
395
396       afile.setNewlineString(newline);
397       afile.setExportSettings(exportSettings);
398       afile.configureForView(viewpanel);
399
400       // check whether we were given a specific alignment to export, rather than
401       // the one in the viewpanel
402       SequenceI[] seqs = null;
403       if (viewpanel == null || viewpanel.getAlignment() == null
404               || viewpanel.getAlignment() != alignment)
405       {
406         seqs = alignment.getSequencesArray();
407       }
408       else
409       {
410         seqs = viewpanel.getAlignment().getSequencesArray();
411       }
412
413       String afileresp = afile.print(seqs, jvsuffix);
414       if (afile.hasWarningMessage())
415       {
416         System.err.println("Warning raised when writing as " + format
417                 + " : " + afile.getWarningMessage());
418       }
419       return afileresp;
420     } catch (Exception e)
421     {
422       System.err.println("Failed to write alignment as a '"
423               + format.getName() + "' file\n");
424       e.printStackTrace();
425     }
426
427     return null;
428   }
429
430
431   /**
432    * Determines the protocol (i.e DataSourceType.{FILE|PASTE|URL}) for the input
433    * data
434    * 
435    * BH 2018 allows File or String, and can return RELATIVE_URL
436    *
437    * @param dataObject File or String
438    * @return the protocol for the input data
439    */
440   public static DataSourceType checkProtocol(Object dataObject)
441   {
442     if(dataObject instanceof File)
443     {
444       return DataSourceType.FILE;
445     }
446     
447     String data = dataObject.toString();
448     DataSourceType protocol = DataSourceType.PASTE;
449     String ft = data.toLowerCase(Locale.ROOT).trim();
450     if (ft.indexOf("http:") == 0 || ft.indexOf("https:") == 0
451             || ft.indexOf("file:") == 0)
452     {
453       protocol = DataSourceType.URL;
454     }
455     else if (Platform.isJS())
456     {
457       protocol = DataSourceType.RELATIVE_URL;
458     }
459     else if (new File(data).exists())
460     {
461       protocol = DataSourceType.FILE;
462     }
463     return protocol;
464   }
465
466   /**
467    * @param args
468    * @j2sIgnore
469    */
470   public static void main(String[] args)
471   {
472     int i = 0;
473     while (i < args.length)
474     {
475       File f = new File(args[i]);
476       if (f.exists())
477       {
478         try
479         {
480           System.out.println("Reading file: " + f);
481           AppletFormatAdapter afa = new AppletFormatAdapter();
482           Runtime r = Runtime.getRuntime();
483           System.gc();
484           long memf = -r.totalMemory() + r.freeMemory();
485           long t1 = -System.currentTimeMillis();
486           AlignmentI al = afa.readFile(args[i], DataSourceType.FILE,
487                   new IdentifyFile().identify(args[i],
488                           DataSourceType.FILE));
489           t1 += System.currentTimeMillis();
490           System.gc();
491           memf += r.totalMemory() - r.freeMemory();
492           if (al != null)
493           {
494             System.out.println("Alignment contains " + al.getHeight()
495                     + " sequences and " + al.getWidth() + " columns.");
496             try
497             {
498               System.out.println(new AppletFormatAdapter()
499                       .formatSequences(FileFormat.Fasta, al, true));
500             } catch (Exception e)
501             {
502               System.err.println(
503                       "Couln't format the alignment for output as a FASTA file.");
504               e.printStackTrace(System.err);
505             }
506           }
507           else
508           {
509             System.out.println("Couldn't read alignment");
510           }
511           System.out.println("Read took " + (t1 / 1000.0) + " seconds.");
512           System.out.println(
513                   "Difference between free memory now and before is "
514                           + (memf / (1024.0 * 1024.0) * 1.0) + " MB");
515         } catch (Exception e)
516         {
517           System.err.println("Exception when dealing with " + i
518                   + "'th argument: " + args[i] + "\n" + e);
519         }
520       }
521       else
522       {
523         System.err.println("Ignoring argument '" + args[i] + "' (" + i
524                 + "'th)- not a readable file.");
525       }
526       i++;
527     }
528   }
529
530   /**
531    * try to discover how to access the given file as a valid datasource that
532    * will be identified as the given type.
533    *
534    * @param file
535    * @param format
536    * @return protocol that yields the data parsable as the given type
537    */
538   public static DataSourceType resolveProtocol(String file,
539           FileFormatI format)
540   {
541     return resolveProtocol(file, format, false);
542   }
543
544   public static DataSourceType resolveProtocol(String file,
545           FileFormatI format, boolean debug)
546   {
547     // TODO: test thoroughly!
548     DataSourceType protocol = null;
549     if (debug)
550     {
551       System.out.println("resolving datasource started with:\n>>file\n"
552               + file + ">>endfile");
553     }
554
555     // This might throw a security exception in certain browsers
556     // Netscape Communicator for instance.
557     try
558     {
559       boolean rtn = false;
560       InputStream is = System.getSecurityManager().getClass()
561               .getResourceAsStream("/" + file);
562       if (is != null)
563       {
564         rtn = true;
565         is.close();
566       }
567       if (debug)
568       {
569         System.err.println("Resource '" + file + "' was "
570                 + (rtn ? "" : "not") + " located by classloader.");
571       }
572       if (rtn)
573       {
574         protocol = DataSourceType.CLASSLOADER;
575       }
576
577     } catch (Exception ex)
578     {
579       System.err
580               .println("Exception checking resources: " + file + " " + ex);
581     }
582
583     if (file.indexOf("://") > -1)
584     {
585       protocol = DataSourceType.URL;
586     }
587     else
588     {
589       // skipping codebase prepend check.
590       protocol = DataSourceType.FILE;
591     }
592     FileParse fp = null;
593     try
594     {
595       if (debug)
596       {
597         System.out.println(
598                 "Trying to get contents of resource as " + protocol + ":");
599       }
600       fp = new FileParse(file, protocol);
601       if (!fp.isValid())
602       {
603         fp = null;
604       }
605       else
606       {
607         if (debug)
608         {
609           System.out.println("Successful.");
610         }
611       }
612     } catch (Exception e)
613     {
614       if (debug)
615       {
616         System.err.println("Exception when accessing content: " + e);
617       }
618       fp = null;
619     }
620     if (fp == null)
621     {
622       if (debug)
623       {
624         System.out.println("Accessing as paste.");
625       }
626       protocol = DataSourceType.PASTE;
627       fp = null;
628       try
629       {
630         fp = new FileParse(file, protocol);
631         if (!fp.isValid())
632         {
633           fp = null;
634         }
635       } catch (Exception e)
636       {
637         System.err.println("Failed to access content as paste!");
638         e.printStackTrace();
639         fp = null;
640       }
641     }
642     if (fp == null)
643     {
644       return null;
645     }
646     if (format == null)
647     {
648       return protocol;
649     }
650     else
651     {
652       try
653       {
654         FileFormatI idformat = new IdentifyFile().identify(file, protocol);
655         if (idformat == null)
656         {
657           if (debug)
658           {
659             System.out.println("Format not identified. Inaccessible file.");
660           }
661           return null;
662         }
663         if (debug)
664         {
665           System.out.println("Format identified as " + idformat
666                   + "and expected as " + format);
667         }
668         if (idformat.equals(format))
669         {
670           if (debug)
671           {
672             System.out.println("Protocol identified as " + protocol);
673           }
674           return protocol;
675         }
676         else
677         {
678           if (debug)
679           {
680             System.out
681                     .println("File deemed not accessible via " + protocol);
682           }
683           fp.close();
684           return null;
685         }
686       } catch (Exception e)
687       {
688         if (debug)
689         {
690           System.err.println("File deemed not accessible via " + protocol);
691           e.printStackTrace();
692         }
693       }
694     }
695     return null;
696   }
697
698   public AlignmentFileReaderI getAlignFile()
699   {
700     return alignFile;
701   }
702 }