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