JAL-3626 reconciled Platform
[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 jalview.javascript.json.JSON;
24
25 import java.awt.Component;
26 import java.awt.Dimension;
27 import java.awt.Toolkit;
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.net.URL;
38 import java.util.Properties;
39 import java.util.logging.ConsoleHandler;
40 import java.util.logging.Level;
41 import java.util.logging.Logger;
42
43 import javax.swing.SwingUtilities;
44
45 import org.json.simple.parser.JSONParser;
46 import org.json.simple.parser.ParseException;
47
48 import com.stevesoft.pat.Regex;
49
50 import swingjs.api.JSUtilI;
51
52 /**
53  * System platform information used by Applet and Application
54  * 
55  * @author Jim Procter
56  */
57 public class Platform
58 {
59
60   private static boolean isJS = /** @j2sNative true || */
61           false;
62
63   private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
64           isWin = null;
65
66   private static swingjs.api.JSUtilI jsutil;
67
68   static {
69           if (isJS) {
70       try
71       {
72         jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
73       } catch (InstantiationException | IllegalAccessException
74               | ClassNotFoundException e)
75       {
76         e.printStackTrace();
77       }
78           }
79   }
80   // private static Boolean isHeadless = null;
81
82   /**
83    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
84    * 
85    * @return
86    */
87   public static boolean isMac()
88   {
89     return (isMac == null
90             ? (isMac = (System.getProperty("os.name").indexOf("Mac") >= 0))
91             : isMac);
92   }
93
94   /**
95    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
96    * 
97    * @return
98    */
99   public static boolean isWin()
100   {
101     return (isWin == null
102             ? (isWin = (System.getProperty("os.name").indexOf("Win") >= 0))
103             : isWin);
104   }
105
106   /**
107    * 
108    * @return true if HTML5 JavaScript
109    */
110   public static boolean isJS()
111   {
112     return isJS;
113   }
114
115   /**
116    * sorry folks - Macs really are different
117    * 
118    * BH: disabled for SwingJS -- will need to check key-press issues
119    * 
120    * @return true if we do things in a special way.
121    */
122   public static boolean isAMacAndNotJS()
123   {
124     return (isNoJSMac == null ? (isNoJSMac = !isJS && isMac()) : isNoJSMac);
125   }
126
127   /**
128    * Check if we are on a Microsoft plaform...
129    * 
130    * @return true if we have to cope with another platform variation
131    */
132   public static boolean isWindowsAndNotJS()
133   {
134     return (isNoJSWin == null ? (isNoJSWin = !isJS && isWin()) : isNoJSWin);
135   }
136
137   // /**
138   // *
139   // * @return true if we are running in non-interactive no UI mode
140   // */
141   // public static boolean isHeadless()
142   // {
143   // if (isHeadless == null)
144   // {
145   // isHeadless = "true".equals(System.getProperty("java.awt.headless"));
146   // }
147   // return isHeadless;
148   // }
149
150   /**
151    * 
152    * @return nominal maximum command line length for this platform
153    */
154   public static int getMaxCommandLineLength()
155   {
156     // TODO: determine nominal limits for most platforms.
157     return 2046; // this is the max length for a windows NT system.
158   }
159
160   /**
161    * An earlier version of escapeString()? Looks fine to me -- BH
162    * 
163    * Answers the input with every backslash replaced with a double backslash (an
164    * 'escaped' single backslash)
165    * 
166    * @param s
167    * @return
168    */
169   public static String escapeBackslashes(String s)
170   {
171     return s == null ? null : s.replace("\\", "\\\\");
172   }
173
174   /**
175    * Answers true if the mouse event has Meta-down (Command key on Mac) or
176    * Ctrl-down (on other o/s). Note this answers _false_ if the Ctrl key is
177    * pressed instead of the Meta/Cmd key on Mac. To test for Ctrl-pressed on
178    * Mac, you can use e.isPopupTrigger().
179    * 
180    * @param e
181    * @return
182    */
183   public static boolean isControlDown(MouseEvent e)
184   {
185     return isControlDown(e, isMac());
186   }
187
188   /**
189    * Overloaded version of method (to allow unit testing)
190    * 
191    * @param e
192    * @param aMac
193    * @return
194    */
195   protected static boolean isControlDown(MouseEvent e, boolean aMac)
196   {
197     if (!aMac)
198     {
199       return e.isControlDown();
200     }
201     // answer false for right mouse button
202     // shortcut key will be META for a Mac
203     return !e.isPopupTrigger()
204             && (Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
205                     & e.getModifiers()) != 0;
206     // could we use e.isMetaDown() here?
207   }
208
209   // BH: I don't know about that previous method. Here is what SwingJS uses.
210   // Notice the distinction in mouse events. (BUTTON3_MASK == META)
211   //
212   // private static boolean isPopupTrigger(int id, int mods, boolean isWin) {
213   // boolean rt = ((mods & InputEvent.BUTTON3_MASK) != 0);
214   // if (isWin) {
215   // if (id != MouseEvent.MOUSE_RELEASED)
216   // return false;
217   ////
218   //// // Oddly, Windows returns InputEvent.META_DOWN_MASK on release, though
219   //// // BUTTON3_DOWN_MASK for pressed. So here we just accept both.
220   ////
221   //// actually, we can use XXX_MASK, not XXX_DOWN_MASK and avoid this issue,
222   // because
223   //// J2S adds the appropriate extended (0x3FC0) and simple (0x3F) modifiers.
224   ////
225   // return rt;
226   // } else {
227   // // mac, linux, unix
228   // if (id != MouseEvent.MOUSE_PRESSED)
229   // return false;
230   // boolean lt = ((mods & InputEvent.BUTTON1_MASK) != 0);
231   // boolean ctrl = ((mods & InputEvent.CTRL_MASK) != 0);
232   // return rt || (ctrl && lt);
233   // }
234   // }
235   //
236
237   /**
238    * Windows (not Mac, Linux, or Unix) and right button to test for the
239    * right-mouse pressed event in Windows that would have opened a menu or a
240    * Mac.
241    * 
242    * @param e
243    * @return
244    */
245   public static boolean isWinRightButton(MouseEvent e)
246   {
247     // was !isAMac(), but that is true also for Linux and Unix and JS,
248
249     return isWin() && SwingUtilities.isRightMouseButton(e);
250   }
251
252   /**
253    * Windows (not Mac, Linux, or Unix) and middle button -- for mouse wheeling
254    * without pressing the button.
255    * 
256    * @param e
257    * @return
258    */
259   public static boolean isWinMiddleButton(MouseEvent e)
260   {
261     // was !isAMac(), but that is true also for Linux and Unix and JS
262     return isWin() && SwingUtilities.isMiddleMouseButton(e);
263   }
264
265   public static boolean allowMnemonics()
266   {
267     return !isMac();
268   }
269
270   public final static int TIME_RESET = 0;
271
272   public final static int TIME_MARK = 1;
273
274   public static final int TIME_SET = 2;
275
276   public static final int TIME_GET = 3;
277
278   public static long time, mark, set, duration;
279
280   /**
281    * typical usage:
282    * 
283    * Platform.timeCheck(null, Platform.TIME_MARK);
284    * 
285    * ...
286    * 
287    * Platform.timeCheck("some message", Platform.TIME_MARK);
288    * 
289    * reset...[set/mark]n...get
290    * 
291    * @param msg
292    * @param mode
293    */
294   public static void timeCheck(String msg, int mode)
295   {
296     long t = System.currentTimeMillis();
297     switch (mode)
298     {
299     case TIME_RESET:
300       time = mark = t;
301       duration = 0;
302       if (msg != null)
303       {
304         System.err.println("Platform: timer reset\t\t\t" + msg);
305       }
306       break;
307     case TIME_MARK:
308       if (set > 0)
309       {
310         // total time between set/mark points
311         duration += (t - set);
312       }
313       else
314       {
315         if (time == 0)
316         {
317           time = mark = t;
318         }
319         if (msg != null)
320         {
321           System.err.println("Platform: timer mark\t" + ((t - time) / 1000f)
322                   + "\t" + ((t - mark) / 1000f) + "\t" + msg);
323         }
324         mark = t;
325       }
326       break;
327     case TIME_SET:
328       set = t;
329       break;
330     case TIME_GET:
331       if (msg != null)
332       {
333         System.err.println("Platform: timer get\t" + ((t - time) / 1000f)
334                 + "\t" + ((duration) / 1000f) + "\t" + msg);
335       }
336       set = 0;
337       break;
338     }
339   }
340
341   /**
342    * Dump the input stream to an output file.
343    * 
344    * @param is
345    * @param outFile
346    * @throws IOException
347    *           if the file cannot be created or there is a problem reading the
348    *           input stream.
349    */
350   public static void streamToFile(InputStream is, File outFile)
351           throws IOException
352   {
353
354     if (isJS)
355     {
356       jsutil.setFileBytes(outFile, is);
357       return;
358     }
359
360     FileOutputStream fio = new FileOutputStream(outFile);
361     try
362     {
363       byte[] bb = new byte[32 * 1024];
364       int l;
365       while ((l = is.read(bb)) > 0)
366       {
367         fio.write(bb, 0, l);
368       }
369     } finally
370     {
371       fio.close();
372     }
373   }
374
375   /**
376    * Encode the URI using JavaScript encodeURIComponent
377    * 
378    * @param value
379    * @return encoded value
380    */
381   public static String encodeURI(String value)
382   {
383     /**
384      * @j2sNative value = encodeURIComponent(value);
385      */
386     return value;
387   }
388
389   /**
390    * Open the URL using a simple window call if this is JavaScript
391    * 
392    * @param url
393    * @return true if window has been opened
394    */
395   public static boolean openURL(String url) throws IOException
396   {
397     if (!isJS)
398     {
399       BrowserLauncher.openURL(url);
400       return false;
401     }
402     /**
403      * @j2sNative
404      * 
405      * 
406      *            window.open(url);
407      */
408     return true;
409   }
410
411   public static Object parseJSON(InputStream response)
412           throws IOException, ParseException
413   {
414     if (isJS)
415     {
416       return JSON.parse(response);
417     }
418
419     BufferedReader br = null;
420     try
421     {
422       br = new BufferedReader(new InputStreamReader(response, "UTF-8"));
423       return new JSONParser().parse(br);
424     } finally
425     {
426       if (br != null)
427       {
428         try
429         {
430           br.close();
431         } catch (IOException e)
432         {
433           // ignore
434         }
435       }
436     }
437   }
438
439   public static Object parseJSON(String json) throws ParseException
440   {
441     return (isJS ? JSON.parse(json) : new JSONParser().parse(json));
442   }
443
444   public static Object parseJSON(Reader r)
445           throws IOException, ParseException
446   {
447     if (r == null)
448     {
449       return null;
450     }
451
452     if (!isJS)
453     {
454       return new JSONParser().parse(r);
455     }
456     // Using a file reader is not currently supported in SwingJS JavaScript
457
458     if (r instanceof FileReader)
459     {
460       throw new IOException(
461               "StringJS does not support FileReader parsing for JSON -- but it could...");
462     }
463     return JSON.parse(r);
464
465   }
466
467   /**
468    * Retrieve the object's embedded size from a div's style on a page if
469    * embedded in SwingJS.
470    * 
471    * @param frame
472    *          JFrame or JInternalFrame
473    * @param defaultWidth
474    *          use -1 to return null (no default size)
475    * @param defaultHeight
476    * @return the embedded dimensions or null (no default size or not embedded)
477    */
478   public static Dimension getDimIfEmbedded(Component frame,
479           int defaultWidth, int defaultHeight)
480   {
481     Dimension d = null;
482     if (isJS)
483     {
484       d = (Dimension) getEmbeddedAttribute(frame, "dim");
485     }
486     return (d == null && defaultWidth >= 0
487             ? new Dimension(defaultWidth, defaultHeight)
488             : d);
489
490   }
491
492   public static Regex newRegex(String searchString, String replaceString)
493   {
494     ensureRegex();
495     return (replaceString == null ? new Regex(searchString)
496             : new Regex(searchString, replaceString));
497   }
498
499   public static Regex newRegexPerl(String code)
500   {
501     ensureRegex();
502     return Regex.perlCode(code);
503   }
504
505   /**
506    * Initialize Java debug logging. A representative sample -- adapt as desired.
507    */
508   public static void startJavaLogging()
509   {
510     /**
511      * @j2sIgnore
512      */
513     {
514       logClass("java.awt.EventDispatchThread", "java.awt.EventQueue",
515               "java.awt.Component", "java.awt.focus.Component",
516               "java.awt.event.Component",
517               "java.awt.focus.DefaultKeyboardFocusManager");
518     }
519   }
520
521   /**
522    * Initiate Java logging for a given class. Only for Java, not JavaScript;
523    * Allows debugging of complex event processing.
524    * 
525    * @param className
526    */
527   public static void logClass(String... classNames)
528   {
529     /**
530      * @j2sIgnore
531      * 
532      * 
533      */
534     {
535       Logger rootLogger = Logger.getLogger("");
536       rootLogger.setLevel(Level.ALL);
537       ConsoleHandler consoleHandler = new ConsoleHandler();
538       consoleHandler.setLevel(Level.ALL);
539       for (int i = classNames.length; --i >= 0;)
540       {
541         Logger logger = Logger.getLogger(classNames[i]);
542         logger.setLevel(Level.ALL);
543         logger.addHandler(consoleHandler);
544       }
545     }
546   }
547
548   /// JavaScript-only methods ///
549
550   public static void cacheFileData(String path, Object data)
551   {
552     if (isJS && data != null)
553     {
554       jsutil.cachePathData(path, data);
555     }
556   }
557
558   public static void cacheFileData(File file)
559   {
560     if (isJS)
561     {
562       byte[] data = Platform.getFileBytes(file);
563       {
564         if (data != null)
565         {
566           cacheFileData(file.toString(), data);
567         }
568       }
569     }
570   }
571
572   public static byte[] getFileBytes(File f)
573   {
574     return (isJS && f != null ? jsutil.getBytes(f) : null);
575   }
576
577   public static byte[] getFileAsBytes(String fileStr)
578   {
579     if (isJS && fileStr != null)
580     {
581       byte[] bytes = (byte[]) jsutil.getFile(fileStr, false);
582       cacheFileData(fileStr, bytes);
583       return bytes;
584     }
585     return null;
586   }
587
588   @SuppressWarnings("unused")
589   public static String getFileAsString(String url)
590   {
591     if (isJS && url != null)
592     {
593       String ret = (String) jsutil.getFile(url, true);
594       cacheFileData(url, ret);
595       return ret;
596     }
597     return null;
598   }
599
600   public static boolean setFileBytes(File f, String urlstring)
601   {
602     if (isJS && f != null && urlstring != null)
603     {
604       @SuppressWarnings("unused")
605       byte[] bytes = getFileAsBytes(urlstring);
606       jsutil.setFileBytes(f, bytes);
607       return true;
608     }
609     return false;
610   }
611
612   public static void addJ2SBinaryType(String ext)
613   {
614     if (isJS)
615     {
616       jsutil.addBinaryFileType(ext);
617     }
618   }
619
620   public static String getUniqueAppletID()
621   {
622     return (isJS ? (String) jsutil.getAppletAttribute("_uniqueId") : null);
623   }
624
625   /**
626    * Read the Info block for this applet.
627    * 
628    * @param prefix
629    *          "jalview_"
630    * @param p
631    * @return unique id for this applet
632    */
633   public static void readInfoProperties(String prefix, Properties p)
634   {
635     if (isJS)
636     {
637       String id = getUniqueAppletID();
638
639       String key = "";
640       String value = "";
641       @SuppressWarnings("unused")
642       Object info = jsutil.getAppletAttribute("__Info");
643       /**
644        * @j2sNative for (key in info) { value = info[key];
645        */
646
647       if (key.indexOf(prefix) == 0)
648       {
649         System.out.println("Platform id=" + id + " reading Info." + key
650                 + " = " + value);
651         p.put(key, value);
652
653       }
654
655       /**
656        * @j2sNative }
657        */
658     }
659   }
660
661   public static void setAjaxJSON(URL url)
662   {
663     if (isJS)
664     {
665       JSON.setAjax(url);
666     }
667   }
668
669
670   /**
671    * Add a known domain that implements access-control-allow-origin:*
672    * 
673    * These should be reviewed periodically.
674    * 
675    * @param domain
676    *          for a service that is not allowing ajax
677    * 
678    * @author hansonr@stolaf.edu
679    * 
680    */
681   public static void addJ2SDirectDatabaseCall(String domain)
682   {
683
684     if (isJS)
685     {
686       jsutil.addDirectDatabaseCall(domain);
687
688       System.out.println(
689             "Platform adding known access-control-allow-origin * for domain "
690                     + domain);
691     }
692
693   }
694
695   public static void getURLCommandArguments()
696   {
697
698     /**
699      * Retrieve the first query field as command arguments to Jalview. Include
700      * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's __Info.args
701      * element to this value.
702      * 
703      * @j2sNative var a =
704      *            decodeURI((document.location.href.replace("&","?").split("?j2s")[0]
705      *            + "?").split("?")[1].split("#")[0]); a &&
706      *            (J2S.thisApplet.__Info.args = a.split(" "));
707      */
708
709   }
710
711   /**
712    * A (case sensitive) file path comparator that ignores the difference between
713    * / and \
714    * 
715    * @param path1
716    * @param path2
717    * @return
718    */
719   public static boolean pathEquals(String path1, String path2)
720   {
721     if (path1 == null)
722     {
723       return path2 == null;
724     }
725     if (path2 == null)
726     {
727       return false;
728     }
729     String p1 = path1.replace('\\', '/');
730     String p2 = path2.replace('\\', '/');
731     return p1.equals(p2);
732   }
733
734   public static URL getDocumentBase()
735   {
736     return (isJS ? jsutil.getDocumentBase() : null);
737   }
738
739   public static URL getCodeBase()
740   {
741     return (isJS ? jsutil.getCodeBase() : null);
742   }
743
744   /**
745    * load a resource -- probably a core file -- if and only if a particular
746    * class has not been instantialized. We use a String here because if we used
747    * a .class object, that reference itself would simply load the class, and we
748    * want the core package to include that as well.
749    * 
750    * @param resourcePath
751    * @param className
752    */
753   public static void loadStaticResource(String resourcePath,
754           String className)
755   {
756     if (isJS)
757     {
758       jsutil.loadResourceIfClassUnknown(resourcePath, className);
759     }
760   }
761
762   public static void ensureRegex()
763   {
764     if (isJS)
765     {
766       loadStaticResource("core/core_stevesoft.z.js",
767               "com.stevesoft.pat.Regex");
768     }
769   }
770
771   /**
772    * Set the "app" property of the HTML5 applet object, for example,
773    * "testApplet.app", to point to the Jalview instance. This will be the object
774    * that page developers use that is similar to the original Java applet object
775    * that was accessed via LiveConnect.
776    * 
777    * @param j
778    */
779   public static void setAppClass(Object j)
780   {
781     if (isJS)
782     {
783       jsutil.setAppletAttribute("app", j);
784     }
785   }
786
787   /**
788    *
789    * If this frame ia embedded in a web page, return a known type.
790    * 
791    * @param frame
792    *          a JFrame or JInternalFrame
793    * @param type
794    * @return null if frame is not embedded.
795    */
796   public static Object getEmbeddedAttribute(Component frame, String type)
797   {
798     return (isJS ? jsutil.getEmbeddedAttribute(frame, type) : null);
799   }
800
801   public static void stackTrace()
802   {
803     try
804     {
805       throw new NullPointerException();
806     } catch (Exception e)
807     {
808       e.printStackTrace();
809     }
810
811   }
812
813 }