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