JAL-3446 JavaScript interface
[jalview.git] / src / jalview / util / Platform.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.util;
22
23 import java.awt.Component;
24 import java.awt.Dimension;
25 import java.awt.Toolkit;
26 import java.awt.event.MouseEvent;
27 import java.io.BufferedReader;
28 import java.io.File;
29 import java.io.FileOutputStream;
30 import java.io.FileReader;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.InputStreamReader;
34 import java.io.Reader;
35 import java.net.URL;
36 import java.nio.channels.Channels;
37 import java.nio.channels.ReadableByteChannel;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.nio.file.Paths;
41 import java.nio.file.StandardCopyOption;
42 import java.nio.file.attribute.BasicFileAttributes;
43 import java.util.Date;
44 import java.util.Locale;
45 import java.util.Map;
46 import java.util.Properties;
47 import java.util.logging.ConsoleHandler;
48 import java.util.logging.Level;
49 import java.util.logging.Logger;
50
51 import javax.swing.SwingUtilities;
52
53 import org.json.simple.parser.JSONParser;
54 import org.json.simple.parser.ParseException;
55
56 import com.stevesoft.pat.Regex;
57
58 import jalview.bin.Jalview;
59 import jalview.javascript.json.JSON;
60 import swingjs.api.JSUtilI;
61
62 /**
63  * System platform information used by Applet and Application
64  * 
65  * @author Jim Procter
66  */
67 public class Platform
68 {
69
70   private static boolean isJS = /** @j2sNative true || */
71           false;
72
73   private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
74           isWin = null;
75
76   private static Boolean isHeadless = null;
77
78   private static swingjs.api.JSUtilI jsutil;
79
80   static
81   {
82     if (isJS)
83     {
84       try
85       {
86         // this is ok - it's a highly embedded method in Java; the deprecation
87         // is
88         // really a recommended best practice.
89         jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
90       } catch (InstantiationException | IllegalAccessException
91               | ClassNotFoundException e)
92       {
93         e.printStackTrace();
94       }
95     }
96   }
97   // private static Boolean isHeadless = null;
98
99   /**
100    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
101    * 
102    * @return
103    */
104   public static boolean isMac()
105   {
106     return (isMac == null
107             ? (isMac = (System.getProperty("os.name").indexOf("Mac") >= 0))
108             : isMac);
109   }
110
111   /**
112    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
113    * 
114    * @return
115    */
116   public static boolean isWin()
117   {
118     return (isWin == null
119             ? (isWin = (System.getProperty("os.name").indexOf("Win") >= 0))
120             : isWin);
121   }
122
123   /**
124    * 
125    * @return true if HTML5 JavaScript
126    */
127   public static boolean isJS()
128   {
129     return isJS;
130   }
131
132   /**
133    * sorry folks - Macs really are different
134    * 
135    * BH: disabled for SwingJS -- will need to check key-press issues
136    * 
137    * @return true if we do things in a special way.
138    */
139   public static boolean isAMacAndNotJS()
140   {
141     return (isNoJSMac == null ? (isNoJSMac = !isJS && isMac()) : isNoJSMac);
142   }
143
144   /**
145    * Check if we are on a Microsoft plaform...
146    * 
147    * @return true if we have to cope with another platform variation
148    */
149   public static boolean isWindowsAndNotJS()
150   {
151     return (isNoJSWin == null ? (isNoJSWin = !isJS && isWin()) : isNoJSWin);
152   }
153
154   /**
155    *
156    * @return true if we are running in non-interactive no UI mode
157    */
158   public static boolean isHeadless()
159   {
160     if (isHeadless == null)
161     {
162       isHeadless = "true".equals(System.getProperty("java.awt.headless"));
163     }
164     return isHeadless;
165   }
166
167   /**
168    * 
169    * @return nominal maximum command line length for this platform
170    */
171   public static int getMaxCommandLineLength()
172   {
173     // TODO: determine nominal limits for most platforms.
174     return 2046; // this is the max length for a windows NT system.
175   }
176
177   /**
178    * Answers the input with every backslash replaced with a double backslash (an
179    * 'escaped' single backslash)
180    * 
181    * @param s
182    * @return
183    */
184   public static String escapeBackslashes(String s)
185   {
186     return s == null ? null : s.replace("\\", "\\\\");
187   }
188
189   /**
190    * Answers true if the mouse event has Meta-down (Command key on Mac) or
191    * Ctrl-down (on other o/s). Note this answers _false_ if the Ctrl key is
192    * pressed instead of the Meta/Cmd key on Mac. To test for Ctrl-pressed on
193    * Mac, you can use e.isPopupTrigger().
194    * 
195    * @param e
196    * @return
197    */
198   public static boolean isControlDown(MouseEvent e)
199   {
200     return isControlDown(e, isMac());
201   }
202
203   /**
204    * Overloaded version of method (to allow unit testing)
205    * 
206    * @param e
207    * @param aMac
208    * @return
209    */
210   protected static boolean isControlDown(MouseEvent e, boolean aMac)
211   {
212     if (!aMac)
213     {
214       return e.isControlDown();
215
216       // Jalview 2.11 code below: above is as amended for JalviewJS
217       // /*
218       // * answer false for right mouse button
219       // */
220       // if (e.isPopupTrigger())
221       // {
222       // return false;
223       // }
224       // return
225       // (jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx() //
226       // .getMenuShortcutKeyMaskEx()
227       // & jalview.util.ShortcutKeyMaskExWrapper
228       // .getModifiersEx(e)) != 0; // getModifiers()) != 0;
229     }
230     // answer false for right mouse button
231     // shortcut key will be META for a Mac
232     return !e.isPopupTrigger()
233             && (Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
234                     & e.getModifiers()) != 0;
235     // could we use e.isMetaDown() here?
236   }
237
238   // BH: I don't know about that previous method. Here is what SwingJS uses.
239   // Notice the distinction in mouse events. (BUTTON3_MASK == META)
240   //
241   // private static boolean isPopupTrigger(int id, int mods, boolean isWin) {
242   // boolean rt = ((mods & InputEvent.BUTTON3_MASK) != 0);
243   // if (isWin) {
244   // if (id != MouseEvent.MOUSE_RELEASED)
245   // return false;
246   ////
247   //// // Oddly, Windows returns InputEvent.META_DOWN_MASK on release, though
248   //// // BUTTON3_DOWN_MASK for pressed. So here we just accept both.
249   ////
250   //// actually, we can use XXX_MASK, not XXX_DOWN_MASK and avoid this issue,
251   // because
252   //// J2S adds the appropriate extended (0x3FC0) and simple (0x3F) modifiers.
253   ////
254   // return rt;
255   // } else {
256   // // mac, linux, unix
257   // if (id != MouseEvent.MOUSE_PRESSED)
258   // return false;
259   // boolean lt = ((mods & InputEvent.BUTTON1_MASK) != 0);
260   // boolean ctrl = ((mods & InputEvent.CTRL_MASK) != 0);
261   // return rt || (ctrl && lt);
262   // }
263   // }
264   //
265
266   /**
267    * Windows (not Mac, Linux, or Unix) and right button to test for the
268    * right-mouse pressed event in Windows that would have opened a menu or a
269    * Mac.
270    * 
271    * @param e
272    * @return
273    */
274   public static boolean isWinRightButton(MouseEvent e)
275   {
276     // was !isAMac(), but that is true also for Linux and Unix and JS,
277
278     return isWin() && SwingUtilities.isRightMouseButton(e);
279   }
280
281   /**
282    * Windows (not Mac, Linux, or Unix) and middle button -- for mouse wheeling
283    * without pressing the button.
284    * 
285    * @param e
286    * @return
287    */
288   public static boolean isWinMiddleButton(MouseEvent e)
289   {
290     // was !isAMac(), but that is true also for Linux and Unix and JS
291     return isWin() && SwingUtilities.isMiddleMouseButton(e);
292   }
293
294   public static boolean allowMnemonics()
295   {
296     return !isMac();
297   }
298
299   public final static int TIME_RESET = 0;
300
301   public final static int TIME_MARK = 1;
302
303   public static final int TIME_SET = 2;
304
305   public static final int TIME_GET = 3;
306
307   public static long time, mark, set, duration;
308
309   /**
310    * typical usage:
311    * 
312    * Platform.timeCheck(null, Platform.TIME_MARK);
313    * 
314    * ...
315    * 
316    * Platform.timeCheck("some message", Platform.TIME_MARK);
317    * 
318    * reset...[set/mark]n...get
319    * 
320    * @param msg
321    * @param mode
322    */
323   public static void timeCheck(String msg, int mode)
324   {
325     long t = System.currentTimeMillis();
326     switch (mode)
327     {
328     case TIME_RESET:
329       time = mark = t;
330       duration = 0;
331       if (msg != null)
332       {
333         System.err.println("Platform: timer reset\t\t\t" + msg);
334       }
335       break;
336     case TIME_MARK:
337       if (set > 0)
338       {
339         // total time between set/mark points
340         duration += (t - set);
341       }
342       else
343       {
344         if (time == 0)
345         {
346           time = mark = t;
347         }
348         if (msg != null)
349         {
350           System.err.println("Platform: timer mark\t" + ((t - time) / 1000f)
351                   + "\t" + ((t - mark) / 1000f) + "\t" + msg);
352         }
353         mark = t;
354       }
355       break;
356     case TIME_SET:
357       set = t;
358       break;
359     case TIME_GET:
360       if (msg != null)
361       {
362         System.err.println("Platform: timer dur\t" + ((t - time) / 1000f)
363                 + "\t" + ((duration) / 1000f) + "\t" + msg);
364       }
365       set = 0;
366       break;
367     }
368   }
369
370   public static void cacheFileData(String path, Object data)
371   {
372     if (isJS && data != null)
373     {
374       jsutil.cachePathData(path, data);
375     }
376   }
377
378   public static void cacheFileData(File file)
379   {
380     if (isJS)
381     {
382       byte[] data = Platform.getFileBytes(file);
383       {
384         if (data != null)
385         {
386           cacheFileData(file.toString(), data);
387         }
388       }
389     }
390   }
391
392   public static byte[] getFileBytes(File f)
393   {
394     return (isJS && f != null ? jsutil.getBytes(f) : null);
395   }
396
397   public static byte[] getFileAsBytes(String fileStr)
398   {
399     if (isJS && fileStr != null)
400     {
401       byte[] bytes = (byte[]) jsutil.getFile(fileStr, false);
402       cacheFileData(fileStr, bytes);
403       return bytes;
404     }
405     return null;
406   }
407
408   public static String getFileAsString(String url)
409   {
410     if (isJS && url != null)
411     {
412       String ret = (String) jsutil.getFile(url, true);
413       cacheFileData(url, ret);
414       return ret;
415     }
416     return null;
417   }
418
419   public static boolean setFileBytes(File f, String urlstring)
420   {
421     if (isJS && f != null && urlstring != null)
422     {
423       @SuppressWarnings("unused")
424       byte[] bytes = getFileAsBytes(urlstring);
425       jsutil.setFileBytes(f, bytes);
426       return true;
427     }
428     return false;
429   }
430
431   public static void addJ2SBinaryType(String ext)
432   {
433     if (isJS)
434     {
435       jsutil.addBinaryFileType(ext);
436     }
437   }
438
439   /**
440    * Encode the URI using JavaScript encodeURIComponent
441    * 
442    * @param value
443    * @return encoded value
444    */
445   public static String encodeURI(String value)
446   {
447     /**
448      * @j2sNative value = encodeURIComponent(value);
449      */
450     return value;
451   }
452
453   /**
454    * Open the URL using a simple window call if this is JavaScript
455    * 
456    * @param url
457    * @return true if window has been opened
458    */
459   public static boolean openURL(String url) throws IOException
460   {
461     if (!isJS())
462     {
463       return false;
464     }
465     /**
466      * @j2sNative
467      * 
468      * 
469      *            window.open(url);
470      */
471     return true;
472   }
473
474   public static String getUniqueAppletID()
475   {
476     return (isJS ? (String) jsutil.getAppletAttribute("_uniqueId") : null);
477   }
478
479   /**
480    * Read the Info block for this applet.
481    * 
482    * @param prefix
483    *          "jalview_"
484    * @param p
485    * @return unique id for this applet
486    */
487   public static void readInfoProperties(String prefix, Properties p)
488   {
489     if (isJS)
490     {
491       String id = getUniqueAppletID();
492
493       String key = "";
494       String value = "";
495       @SuppressWarnings("unused")
496       Object info = jsutil.getAppletAttribute("__Info");
497       /**
498        * @j2sNative for (key in info) { value = info[key];
499        */
500
501       if (key.indexOf(prefix) == 0)
502       {
503         System.out.println("Platform id=" + id + " reading Info." + key
504                 + " = " + value);
505         p.put(key, value);
506
507       }
508
509       /**
510        * @j2sNative }
511        */
512     }
513   }
514
515   public static void setAjaxJSON(URL url)
516   {
517     if (isJS())
518     {
519       JSON.setAjax(url);
520     }
521   }
522
523   public static Object parseJSON(InputStream response)
524           throws IOException, ParseException
525   {
526     if (isJS())
527     {
528       return JSON.parse(response);
529     }
530
531     BufferedReader br = null;
532     try
533     {
534       br = new BufferedReader(new InputStreamReader(response, "UTF-8"));
535       return new JSONParser().parse(br);
536     } finally
537     {
538       if (br != null)
539       {
540         try
541         {
542           br.close();
543         } catch (IOException e)
544         {
545           // ignore
546         }
547       }
548     }
549   }
550
551   public static Object parseJSON(String json) throws ParseException
552   {
553     return (isJS() ? JSON.parse(json) : new JSONParser().parse(json));
554   }
555
556   public static Object parseJSON(Reader r)
557           throws IOException, ParseException
558   {
559     if (r == null)
560     {
561       return null;
562     }
563
564     if (!isJS())
565     {
566       return new JSONParser().parse(r);
567     }
568     // Using a file reader is not currently supported in SwingJS JavaScript
569
570     if (r instanceof FileReader)
571     {
572       throw new IOException(
573               "StringJS does not support FileReader parsing for JSON -- but it could...");
574     }
575     return JSON.parse(r);
576   }
577
578   /**
579    * Dump the input stream to an output file.
580    * 
581    * @param is
582    * @param outFile
583    * @throws IOException
584    *           if the file cannot be created or there is a problem reading the
585    *           input stream.
586    */
587   public static void streamToFile(InputStream is, File outFile)
588           throws IOException
589   {
590
591     if (isJS)
592     {
593       jsutil.setFileBytes(outFile, is);
594       return;
595     }
596
597     FileOutputStream fio = new FileOutputStream(outFile);
598     try
599     {
600       byte[] bb = new byte[32 * 1024];
601       int l;
602       while ((l = is.read(bb)) > 0)
603       {
604         fio.write(bb, 0, l);
605       }
606     } finally
607     {
608       fio.close();
609     }
610   }
611
612   /**
613    * Add a known domain that implements access-control-allow-origin:*
614    * 
615    * These should be reviewed periodically.
616    * 
617    * @param domain
618    *          for a service that is not allowing ajax
619    * 
620    * @author hansonr@stolaf.edu
621    * 
622    */
623   public static void addJ2SDirectDatabaseCall(String domain)
624   {
625
626     if (isJS)
627     {
628       jsutil.addDirectDatabaseCall(domain);
629
630       System.out.println(
631               "Platform adding known access-control-allow-origin * for domain "
632                       + domain);
633     }
634
635   }
636
637   /**
638    * Allow for URL-line command arguments. Untested.
639    * 
640    */
641   public static void getURLCommandArguments()
642   {
643
644     try {
645     /**
646      * Retrieve the first query field as command arguments to Jalview. Include
647      * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's __Info.args
648      * element to this value.
649      * 
650      * @j2sNative var a =
651      *            decodeURI((document.location.href.replace("&","?").split("?j2s")[0]
652      *            + "?").split("?")[1].split("#")[0]); a &&
653      *            (J2S.thisApplet.__Info.args = a.split(" "));
654      *            
655      *            System.out.println("URL arguments: " + a);
656      */
657     } catch (Throwable t) {
658     }
659   }
660
661   /**
662    * A (case sensitive) file path comparator that ignores the difference between
663    * / and \
664    * 
665    * @param path1
666    * @param path2
667    * @return
668    */
669   public static boolean pathEquals(String path1, String path2)
670   {
671     if (path1 == null)
672     {
673       return path2 == null;
674     }
675     if (path2 == null)
676     {
677       return false;
678     }
679     String p1 = path1.replace('\\', '/');
680     String p2 = path2.replace('\\', '/');
681     return p1.equals(p2);
682   }
683
684   ///////////// JAL-3253 Applet additions //////////////
685
686   /**
687    * Retrieve the object's embedded size from a div's style on a page if
688    * embedded in SwingJS.
689    * 
690    * @param frame
691    *          JFrame or JInternalFrame
692    * @param defaultWidth
693    *          use -1 to return null (no default size)
694    * @param defaultHeight
695    * @return the embedded dimensions or null (no default size or not embedded)
696    */
697   public static Dimension getDimIfEmbedded(Component frame,
698           int defaultWidth, int defaultHeight)
699   {
700     Dimension d = null;
701     if (isJS)
702     {
703       d = (Dimension) getEmbeddedAttribute(frame, "dim");
704     }
705     return (d == null && defaultWidth >= 0
706             ? new Dimension(defaultWidth, defaultHeight)
707             : d);
708
709   }
710
711   public static Regex newRegex(String regex)
712   {
713     return newRegex(regex, null);
714   }
715
716   public static Regex newRegex(String searchString, String replaceString)
717   {
718     ensureRegex();
719     return (replaceString == null ? new Regex(searchString)
720             : new Regex(searchString, replaceString));
721   }
722
723   public static Regex newRegexPerl(String code)
724   {
725     ensureRegex();
726     return Regex.perlCode(code);
727   }
728
729   /**
730    * Initialize Java debug logging. A representative sample -- adapt as desired.
731    */
732   public static void startJavaLogging()
733   {
734     /**
735      * @j2sIgnore
736      */
737     {
738       logClass("java.awt.EventDispatchThread", "java.awt.EventQueue",
739               "java.awt.Component", "java.awt.focus.Component",
740               "java.awt.event.Component",
741               "java.awt.focus.DefaultKeyboardFocusManager");
742     }
743   }
744
745   /**
746    * Initiate Java logging for a given class. Only for Java, not JavaScript;
747    * Allows debugging of complex event processing.
748    * 
749    * @param className
750    */
751   public static void logClass(String... classNames)
752   {
753     /**
754      * @j2sIgnore
755      * 
756      * 
757      */
758     {
759       Logger rootLogger = Logger.getLogger("");
760       rootLogger.setLevel(Level.ALL);
761       ConsoleHandler consoleHandler = new ConsoleHandler();
762       consoleHandler.setLevel(Level.ALL);
763       for (int i = classNames.length; --i >= 0;)
764       {
765         Logger logger = Logger.getLogger(classNames[i]);
766         logger.setLevel(Level.ALL);
767         logger.addHandler(consoleHandler);
768       }
769     }
770   }
771
772   /**
773    * load a resource -- probably a core file -- if and only if a particular
774    * class has not been instantialized. We use a String here because if we used
775    * a .class object, that reference itself would simply load the class, and we
776    * want the core package to include that as well.
777    * 
778    * @param resourcePath
779    * @param className
780    */
781   public static void loadStaticResource(String resourcePath,
782           String className)
783   {
784     if (isJS)
785     {
786       jsutil.loadResourceIfClassUnknown(resourcePath, className);
787     }
788   }
789
790   public static void ensureRegex()
791   {
792     if (isJS)
793     {
794       loadStaticResource("core/core_stevesoft.z.js",
795               "com.stevesoft.pat.Regex");
796     }
797   }
798
799   /**
800    * Set the "app" property of the HTML5 applet object, for example,
801    * "testApplet.app", to point to the Jalview instance. This will be the object
802    * that page developers use that is similar to the original Java applet object
803    * that was accessed via LiveConnect.
804    * 
805    * @param j
806    */
807   public static void setAppClass(Object j)
808   {
809     if (isJS)
810     {
811       jsutil.setAppClass(j);
812     }
813   }
814
815   /**
816    *
817    * If this frame is embedded in a web page, return a known type.
818    * 
819    * @param frame
820    *          a JFrame or JInternalFrame
821    * @param type "name", "node", "init", "dim", or any DOM attribute, such as "id"
822    * @return null if frame is not embedded.
823    */
824   public static Object getEmbeddedAttribute(Component frame, String type)
825   {
826     return (isJS ? jsutil.getEmbeddedAttribute(frame, type) : null);
827   }
828
829   public static void stackTrace()
830   {
831     try
832     {
833       throw new NullPointerException();
834     } catch (Exception e)
835     {
836       e.printStackTrace();
837     }
838
839   }
840
841   public static URL getDocumentBase()
842   {
843     return (isJS ? jsutil.getDocumentBase() : null);
844   }
845
846   public static URL getCodeBase()
847   {
848     return (isJS ? jsutil.getCodeBase() : null);
849   }
850
851   public static String getUserPath(String subpath)
852   {
853     char sep = File.separatorChar;
854     return System.getProperty("user.home") + sep
855             + subpath.replace('/', sep);
856   }
857
858   /**
859    * This method enables checking if a cached file has exceeded a certain
860    * threshold(in days)
861    * 
862    * @param file
863    *          the cached file
864    * @param noOfDays
865    *          the threshold in days
866    * @return
867    */
868   public static boolean isFileOlderThanThreshold(File file, int noOfDays)
869   {
870     if (isJS())
871     {
872       // not meaningful in SwingJS -- this is a session-specific temp file. It
873       // doesn't have a timestamp.
874       return false;
875     }
876     Path filePath = file.toPath();
877     BasicFileAttributes attr;
878     int diffInDays = 0;
879     try
880     {
881       attr = Files.readAttributes(filePath, BasicFileAttributes.class);
882       diffInDays = (int) ((new Date().getTime()
883               - attr.lastModifiedTime().toMillis())
884               / (1000 * 60 * 60 * 24));
885       // System.out.println("Diff in days : " + diffInDays);
886     } catch (IOException e)
887     {
888       e.printStackTrace();
889     }
890     return noOfDays <= diffInDays;
891   }
892
893   /**
894    * Get the leading integer part of a string that begins with an integer.
895    * 
896    * @param input
897    *          - the string input to process
898    * @param failValue
899    *          - value returned if unsuccessful
900    * @return
901    */
902   public static int getLeadingIntegerValue(String input, int failValue)
903   {
904     if (input == null)
905     {
906       return failValue;
907     }
908     if (isJS)
909     {
910       int val = /** @j2sNative 1 ? parseInt(input) : */
911               0;
912       return (val == val + 0 ? val : failValue);
913     }
914     // JavaScript does not support Regex ? lookahead
915     String[] parts = input.split("(?=\\D)(?<=\\d)");
916     if (parts != null && parts.length > 0 && parts[0].matches("[0-9]+"))
917     {
918       return Integer.valueOf(parts[0]);
919     }
920     return failValue;
921   }
922
923   public static Map<String, Object> getAppletInfoAsMap()
924   {
925     return (isJS ? jsutil.getAppletInfoAsMap() : null);
926   }
927
928   /**
929    * Get the SwingJS applet ID and combine that with the frameType
930    * 
931    * @param frameType
932    *          "alignment", "desktop", etc., or null
933    * @return
934    */
935   public static String getAppID(String frameType)
936   {
937     
938     String id = Jalview.getInstance().j2sAppletID;
939     if (id == null)
940     {
941       Jalview.getInstance().j2sAppletID = id = (isJS ? (String) jsutil
942               .getAppletAttribute("_id") : "jalview");
943     }
944     return id + (frameType == null ? "" : "-" + frameType);
945   }
946
947
948   /**
949    * Option to avoid unnecessary seeking of nonexistent resources in JavaScript.
950    * Works in Java as well.
951    * 
952    * @param loc
953    * @return
954    */
955   public static Locale getLocaleOrNone(Locale loc)
956   {
957     return (isJS && loc.getLanguage() == "en" ? new Locale("") : loc);
958   }
959
960   /**
961    * From UrlDownloadClient; trivial in JavaScript; painful in Java.
962    * 
963    * @param urlstring
964    * @param outfile
965    * @throws IOException
966    */
967   public static void download(String urlstring, String outfile)
968           throws IOException
969   {
970     Path temp = null;
971     try (InputStream is = new URL(urlstring).openStream())
972     {
973       if (isJS)
974       { // so much easier!
975         streamToFile(is, new File(outfile));
976         return;
977       }
978       temp = Files.createTempFile(".jalview_", ".tmp");
979       try (FileOutputStream fos = new FileOutputStream(temp.toString());
980               ReadableByteChannel rbc = Channels.newChannel(is))
981       {
982         fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
983         // copy tempfile to outfile once our download completes
984         // incase something goes wrong
985         Files.copy(temp, Paths.get(outfile),
986                 StandardCopyOption.REPLACE_EXISTING);
987       }
988     } catch (IOException e)
989     {
990       throw e;
991     } finally
992     {
993       try
994       {
995         if (temp != null)
996         {
997           Files.deleteIfExists(temp);
998         }
999       } catch (IOException e)
1000       {
1001         System.out.println("Exception while deleting download temp file: "
1002                 + e.getMessage());
1003       }
1004     }
1005   }
1006
1007 }