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