JAL-3005 refactor updateRecentlyOpened to enable reuse
[jalview.git] / src / jalview / io / FileLoader.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.ComplexAlignFile;
24 import jalview.api.FeatureSettingsModelI;
25 import jalview.api.FeaturesDisplayedI;
26 import jalview.api.FeaturesSourceI;
27 import jalview.bin.Cache;
28 import jalview.bin.Jalview;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.HiddenColumns;
31 import jalview.datamodel.PDBEntry;
32 import jalview.datamodel.SequenceI;
33 import jalview.gui.AlignFrame;
34 import jalview.gui.AlignViewport;
35 import jalview.gui.Desktop;
36 import jalview.gui.Jalview2XML;
37 import jalview.gui.JvOptionPane;
38 import jalview.json.binding.biojson.v1.ColourSchemeMapper;
39 import jalview.schemes.ColourSchemeI;
40 import jalview.structure.StructureSelectionManager;
41 import jalview.util.MessageManager;
42 import jalview.ws.utils.UrlDownloadClient;
43
44 import java.io.File;
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.StringTokenizer;
49
50 import javax.swing.SwingUtilities;
51
52 public class FileLoader implements Runnable
53 {
54   private static final String TAB = "\t";
55
56   String file;
57
58   DataSourceType protocol;
59
60   FileFormatI format;
61
62   AlignmentFileReaderI source = null; // alternative specification of where data
63                                       // comes
64
65   // from
66
67   AlignViewport viewport;
68
69   AlignFrame alignFrame;
70
71   long loadtime;
72
73   long memused;
74
75   boolean raiseGUI = true;
76
77   /**
78    * default constructor always raised errors in GUI dialog boxes
79    */
80   public FileLoader()
81   {
82     this(true);
83   }
84
85   /**
86    * construct a Fileloader that may raise errors non-interactively
87    * 
88    * @param raiseGUI
89    *          true if errors are to be raised as GUI dialog boxes
90    */
91   public FileLoader(boolean raiseGUI)
92   {
93     this.raiseGUI = raiseGUI;
94   }
95
96   public void LoadFile(AlignViewport viewport, String file,
97           DataSourceType protocol, FileFormatI format)
98   {
99     this.viewport = viewport;
100     LoadFile(file, protocol, format);
101   }
102
103   public void LoadFile(String file, DataSourceType protocol,
104           FileFormatI format)
105   {
106     this.file = file;
107     this.protocol = protocol;
108     this.format = format;
109
110     final Thread loader = new Thread(this);
111
112     SwingUtilities.invokeLater(new Runnable()
113     {
114       @Override
115       public void run()
116       {
117         loader.start();
118       }
119     });
120   }
121
122   /**
123    * Load a (file, protocol) source of unknown type
124    * 
125    * @param file
126    * @param protocol
127    */
128   public void LoadFile(String file, DataSourceType protocol)
129   {
130     LoadFile(file, protocol, null);
131   }
132
133   /**
134    * Load alignment from (file, protocol) and wait till loaded
135    * 
136    * @param file
137    * @param sourceType
138    * @return alignFrame constructed from file contents
139    */
140   public AlignFrame LoadFileWaitTillLoaded(String file,
141           DataSourceType sourceType)
142   {
143     return LoadFileWaitTillLoaded(file, sourceType, null);
144   }
145
146   /**
147    * Load alignment from (file, protocol) of type format and wait till loaded
148    * 
149    * @param file
150    * @param sourceType
151    * @param format
152    * @return alignFrame constructed from file contents
153    */
154   public AlignFrame LoadFileWaitTillLoaded(String file,
155           DataSourceType sourceType, FileFormatI format)
156   {
157     this.file = file;
158     this.protocol = sourceType;
159     this.format = format;
160     return _LoadFileWaitTillLoaded();
161   }
162
163   /**
164    * Load alignment from FileParse source of type format and wait till loaded
165    * 
166    * @param source
167    * @param format
168    * @return alignFrame constructed from file contents
169    */
170   public AlignFrame LoadFileWaitTillLoaded(AlignmentFileReaderI source,
171           FileFormatI format)
172   {
173     this.source = source;
174
175     file = source.getInFile();
176     protocol = source.getDataSourceType();
177     this.format = format;
178     return _LoadFileWaitTillLoaded();
179   }
180
181   /**
182    * start thread and wait until finished, then return the alignFrame that's
183    * (hopefully) been read.
184    * 
185    * @return
186    */
187   protected AlignFrame _LoadFileWaitTillLoaded()
188   {
189     Thread loader = new Thread(this);
190     loader.start();
191
192     while (loader.isAlive())
193     {
194       try
195       {
196         Thread.sleep(500);
197       } catch (Exception ex)
198       {
199       }
200     }
201
202     return alignFrame;
203   }
204
205   public void LoadFileOntoAlignmentWaitTillLoaded(AlignViewport viewport,
206           String file, DataSourceType sourceType, FileFormatI format)
207   {
208     this.viewport = viewport;
209     this.file = file;
210     this.protocol = sourceType;
211     this.format = format;
212     _LoadAlignmentFileWaitTillLoaded();
213   }
214
215   protected void _LoadAlignmentFileWaitTillLoaded()
216   {
217     Thread loader = new Thread(this);
218     loader.start();
219
220     while (loader.isAlive())
221     {
222       try
223       {
224         Thread.sleep(500);
225       } catch (Exception ex)
226       {
227       }
228     }
229   }
230
231   /**
232    * Updates (or creates) the tab-separated list of recently opened files held
233    * under the given property name by inserting the filePath at the front of the
234    * list. Duplicates are removed, and the list is limited to 11 entries. The
235    * method returns the updated value of the property.
236    * 
237    * @param filePath
238    * @param sourceType
239    */
240   public static String updateRecentlyOpened(String filePath,
241           DataSourceType sourceType)
242   {
243     if (sourceType != DataSourceType.FILE
244             && sourceType != DataSourceType.URL)
245     {
246       return null;
247     }
248
249     String propertyName = sourceType == DataSourceType.FILE ? "RECENT_FILE"
250             : "RECENT_URL";
251     String historyItems = Cache.getProperty(propertyName);
252     if (filePath != null
253             && filePath.indexOf(System.getProperty("java.io.tmpdir")) > -1)
254     {
255       // ignore files loaded from the system's temporary directory
256       return null;
257     }
258
259     List<String> recent = new ArrayList<>();
260
261     if (historyItems != null)
262     {
263       StringTokenizer st = new StringTokenizer(historyItems, TAB);
264
265       while (st.hasMoreTokens())
266       {
267         String trimmed = st.nextToken().trim();
268         if (!recent.contains(trimmed))
269         {
270           recent.add(trimmed);
271         }
272       }
273     }
274
275     /*
276      * if file was already in the list, it moves to the top
277      */
278     if (recent.contains(filePath))
279     {
280       recent.remove(filePath);
281     }
282
283     StringBuilder newHistory = new StringBuilder(filePath);
284     for (int i = 0; i < recent.size() && i < 10; i++)
285     {
286       newHistory.append(TAB);
287       newHistory.append(recent.get(i));
288     }
289
290     String newProperty = newHistory.toString();
291     Cache.setProperty(propertyName, newProperty);
292
293     return newProperty;
294   }
295
296   @Override
297   public void run()
298   {
299     String title = protocol == DataSourceType.PASTE
300             ? "Copied From Clipboard"
301             : file;
302     Runtime rt = Runtime.getRuntime();
303     try
304     {
305       if (Desktop.instance != null)
306       {
307         Desktop.instance.startLoading(file);
308       }
309       if (format == null)
310       {
311         // just in case the caller didn't identify the file for us
312         if (source != null)
313         {
314           format = new IdentifyFile().identify(source, false);
315           // identify stream and rewind rather than close
316         }
317         else
318         {
319           format = new IdentifyFile().identify(file, protocol);
320         }
321
322       }
323
324       if (format == null)
325       {
326         Desktop.instance.stopLoading();
327         System.err.println("The input file \"" + file
328                 + "\" has null or unidentifiable data content!");
329         if (!Jalview.isHeadlessMode())
330         {
331           JvOptionPane.showInternalMessageDialog(Desktop.desktop,
332                   MessageManager.getString("label.couldnt_read_data")
333                           + " in " + file + "\n"
334                           + AppletFormatAdapter.getSupportedFormats(),
335                   MessageManager.getString("label.couldnt_read_data"),
336                   JvOptionPane.WARNING_MESSAGE);
337         }
338         return;
339       }
340       // TODO: cache any stream datasources as a temporary file (eg. PDBs
341       // retrieved via URL)
342       if (Desktop.desktop != null && Desktop.desktop.isShowMemoryUsage())
343       {
344         System.gc();
345         memused = (rt.maxMemory() - rt.totalMemory() + rt.freeMemory()); // free
346         // memory
347         // before
348         // load
349       }
350       loadtime = -System.currentTimeMillis();
351       AlignmentI al = null;
352
353       if (FileFormat.Jalview.equals(format))
354       {
355         if (source != null)
356         {
357           // Tell the user (developer?) that this is going to cause a problem
358           System.err.println(
359                   "IMPLEMENTATION ERROR: Cannot read consecutive Jalview XML projects from a stream.");
360           // We read the data anyway - it might make sense.
361         }
362         alignFrame = new Jalview2XML(raiseGUI).loadJalviewAlign(file);
363       }
364       else
365       {
366         String error = AppletFormatAdapter.getSupportedFormats();
367         try
368         {
369           if (source != null)
370           {
371             // read from the provided source
372             al = new FormatAdapter().readFromFile(source, format);
373           }
374           else
375           {
376
377             // open a new source and read from it
378             FormatAdapter fa = new FormatAdapter();
379             boolean downloadStructureFile = format.isStructureFile()
380                     && protocol.equals(DataSourceType.URL);
381             if (downloadStructureFile)
382             {
383               String structExt = format.getExtensions().split(",")[0];
384               String urlLeafName = file.substring(
385                       file.lastIndexOf(
386                               System.getProperty("file.separator")),
387                       file.lastIndexOf("."));
388               String tempStructureFileStr = createNamedJvTempFile(
389                       urlLeafName, structExt);
390               UrlDownloadClient.download(file, tempStructureFileStr);
391               al = fa.readFile(tempStructureFileStr, DataSourceType.FILE,
392                       format);
393               source = fa.getAlignFile();
394             }
395             else
396             {
397               al = fa.readFile(file, protocol, format);
398               source = fa.getAlignFile(); // keep reference for later if
399                                           // necessary.
400             }
401           }
402         } catch (java.io.IOException ex)
403         {
404           error = ex.getMessage();
405         }
406
407         if ((al != null) && (al.getHeight() > 0) && al.hasValidSequence())
408         {
409           // construct and register dataset sequences
410           for (SequenceI sq : al.getSequences())
411           {
412             while (sq.getDatasetSequence() != null)
413             {
414               sq = sq.getDatasetSequence();
415             }
416             if (sq.getAllPDBEntries() != null)
417             {
418               for (PDBEntry pdbe : sq.getAllPDBEntries())
419               {
420                 // register PDB entries with desktop's structure selection
421                 // manager
422                 StructureSelectionManager
423                         .getStructureSelectionManager(Desktop.instance)
424                         .registerPDBEntry(pdbe);
425               }
426             }
427           }
428
429           FeatureSettingsModelI proxyColourScheme = source
430                   .getFeatureColourScheme();
431           if (viewport != null)
432           {
433             if (proxyColourScheme != null)
434             {
435               viewport.applyFeaturesStyle(proxyColourScheme);
436             }
437             // append to existing alignment
438             viewport.addAlignment(al, title);
439             if (source instanceof HMMFile)
440             {
441               AlignmentI alignment = viewport.getAlignment();
442               SequenceI seq = alignment
443                       .getSequenceAt(alignment.getAbsoluteHeight() - 1);
444               if (seq.hasHMMProfile())
445               {
446                 /* 
447                  * fudge: move HMM consensus sequence from last to first
448                  */
449                 alignment.deleteSequence(alignment.getAbsoluteHeight() - 1);
450                 alignment.insertSequenceAt(0, seq);
451               }
452               viewport.getAlignPanel().adjustAnnotationHeight();
453               viewport.updateSequenceIdColours();
454             }
455           }
456           else
457           {
458             // otherwise construct the alignFrame
459
460             if (source instanceof ComplexAlignFile)
461             {
462               HiddenColumns colSel = ((ComplexAlignFile) source)
463                       .getHiddenColumns();
464               SequenceI[] hiddenSeqs = ((ComplexAlignFile) source)
465                       .getHiddenSequences();
466               String colourSchemeName = ((ComplexAlignFile) source)
467                       .getGlobalColourScheme();
468               FeaturesDisplayedI fd = ((ComplexAlignFile) source)
469                       .getDisplayedFeatures();
470               alignFrame = new AlignFrame(al, hiddenSeqs, colSel,
471                       AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
472               alignFrame.getViewport().setFeaturesDisplayed(fd);
473               alignFrame.getViewport().setShowSequenceFeatures(
474                       ((ComplexAlignFile) source).isShowSeqFeatures());
475               ColourSchemeI cs = ColourSchemeMapper
476                       .getJalviewColourScheme(colourSchemeName, al);
477               if (cs != null)
478               {
479                 alignFrame.changeColour(cs);
480               }
481             }
482             else
483             {
484               alignFrame = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
485                       AlignFrame.DEFAULT_HEIGHT);
486               if (source instanceof FeaturesSourceI)
487               {
488                 alignFrame.getViewport().setShowSequenceFeatures(true);
489               }
490             }
491             // add metadata and update ui
492             if (!(protocol == DataSourceType.PASTE))
493             {
494               alignFrame.setFileName(file, format);
495             }
496             if (proxyColourScheme != null)
497             {
498               alignFrame.getViewport()
499                       .applyFeaturesStyle(proxyColourScheme);
500             }
501             alignFrame.statusBar.setText(MessageManager.formatMessage(
502                     "label.successfully_loaded_file", new String[]
503                     { title }));
504
505             if (raiseGUI)
506             {
507               // add the window to the GUI
508               // note - this actually should happen regardless of raiseGUI
509               // status in Jalview 3
510               // TODO: define 'virtual desktop' for benefit of headless scripts
511               // that perform queries to find the 'current working alignment'
512               Desktop.addInternalFrame(alignFrame, title,
513                       AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
514             }
515
516             try
517             {
518               alignFrame.setMaximum(jalview.bin.Cache
519                       .getDefault("SHOW_FULLSCREEN", false));
520             } catch (java.beans.PropertyVetoException ex)
521             {
522             }
523           }
524         }
525         else
526         {
527           if (Desktop.instance != null)
528           {
529             Desktop.instance.stopLoading();
530           }
531
532           final String errorMessage = MessageManager.getString(
533                   "label.couldnt_load_file") + " " + title + "\n" + error;
534           // TODO: refactor FileLoader to be independent of Desktop / Applet GUI
535           // bits ?
536           if (raiseGUI && Desktop.desktop != null)
537           {
538             javax.swing.SwingUtilities.invokeLater(new Runnable()
539             {
540               @Override
541               public void run()
542               {
543                 JvOptionPane.showInternalMessageDialog(Desktop.desktop,
544                         errorMessage,
545                         MessageManager
546                                 .getString("label.error_loading_file"),
547                         JvOptionPane.WARNING_MESSAGE);
548               }
549             });
550           }
551           else
552           {
553             System.err.println(errorMessage);
554           }
555         }
556       }
557
558       updateRecentlyOpened(file, protocol);
559
560       if (protocol == DataSourceType.FILE && format != null)
561       {
562         Cache.setProperty("DEFAULT_FILE_FORMAT", format.getName());
563       }
564
565     } catch (Exception er)
566     {
567       System.err.println("Exception whilst opening file '" + file);
568       er.printStackTrace();
569       if (raiseGUI)
570       {
571         javax.swing.SwingUtilities.invokeLater(new Runnable()
572         {
573           @Override
574           public void run()
575           {
576             JvOptionPane.showInternalMessageDialog(Desktop.desktop,
577                     MessageManager.formatMessage(
578                             "label.problems_opening_file", new String[]
579                             { file }),
580                     MessageManager.getString("label.file_open_error"),
581                     JvOptionPane.WARNING_MESSAGE);
582           }
583         });
584       }
585       alignFrame = null;
586     } catch (OutOfMemoryError er)
587     {
588
589       er.printStackTrace();
590       alignFrame = null;
591       if (raiseGUI)
592       {
593         javax.swing.SwingUtilities.invokeLater(new Runnable()
594         {
595           @Override
596           public void run()
597           {
598             JvOptionPane.showInternalMessageDialog(Desktop.desktop,
599                     MessageManager.formatMessage(
600                             "warn.out_of_memory_loading_file", new String[]
601                             { file }),
602                     MessageManager.getString("label.out_of_memory"),
603                     JvOptionPane.WARNING_MESSAGE);
604           }
605         });
606       }
607       System.err.println("Out of memory loading file " + file + "!!");
608
609     }
610     loadtime += System.currentTimeMillis();
611     // TODO: Estimate percentage of memory used by a newly loaded alignment -
612     // warn if more memory will be needed to work with it
613     // System.gc();
614     memused = memused
615             - (rt.maxMemory() - rt.totalMemory() + rt.freeMemory()); // difference
616     // in free
617     // memory
618     // after
619     // load
620     if (Desktop.desktop != null && Desktop.desktop.isShowMemoryUsage())
621     {
622       if (alignFrame != null)
623       {
624         AlignmentI al = alignFrame.getViewport().getAlignment();
625
626         System.out.println("Loaded '" + title + "' in "
627                 + (loadtime / 1000.0) + "s, took an additional "
628                 + (1.0 * memused / (1024.0 * 1024.0)) + " MB ("
629                 + al.getHeight() + " seqs by " + al.getWidth() + " cols)");
630       }
631       else
632       {
633         // report that we didn't load anything probably due to an out of memory
634         // error
635         System.out.println("Failed to load '" + title + "' in "
636                 + (loadtime / 1000.0) + "s, took an additional "
637                 + (1.0 * memused / (1024.0 * 1024.0))
638                 + " MB (alignment is null)");
639       }
640     }
641     // remove the visual delay indicator
642     if (Desktop.instance != null)
643     {
644       Desktop.instance.stopLoading();
645     }
646
647   }
648
649   /**
650    * This method creates the file -
651    * {tmpdir}/jalview/{current_timestamp}/fileName.exetnsion using the supplied
652    * file name and extension
653    * 
654    * @param fileName
655    *          the name of the temp file to be created
656    * @param extension
657    *          the extension of the temp file to be created
658    * @return
659    */
660   private static String createNamedJvTempFile(String fileName,
661           String extension) throws IOException
662   {
663     String seprator = System.getProperty("file.separator");
664     String jvTempDir = System.getProperty("java.io.tmpdir") + "jalview"
665             + seprator + System.currentTimeMillis();
666     File tempStructFile = new File(
667             jvTempDir + seprator + fileName + "." + extension);
668     tempStructFile.mkdirs();
669     return tempStructFile.toString();
670   }
671
672 }