corrects Platform.openURL
[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       return false;
451     }
452     /**
453      * @j2sNative
454      * 
455      * 
456      *            window.open(url);
457      */
458     return true;
459   }
460
461   public static String getUniqueAppletID()
462   {
463     return (isJS ? (String) jsutil.getAppletAttribute("_uniqueId") : null);
464   }
465
466
467   /**
468    * Read the Info block for this applet.
469    * 
470    * @param prefix
471    *          "jalview_"
472    * @param p
473    * @return unique id for this applet
474    */
475   public static void readInfoProperties(String prefix, Properties p)
476   {
477     if (isJS)
478     {
479       String id = getUniqueAppletID();
480
481       String key = "";
482       String value = "";
483       @SuppressWarnings("unused")
484       Object info = jsutil.getAppletAttribute("__Info");
485       /**
486        * @j2sNative for (key in info) { value = info[key];
487        */
488
489       if (key.indexOf(prefix) == 0)
490       {
491         System.out.println("Platform id=" + id + " reading Info." + key
492                 + " = " + value);
493         p.put(key, value);
494
495       }
496
497       /**
498        * @j2sNative }
499        */
500     }
501   }
502
503   public static void setAjaxJSON(URL url)
504   {
505     if (isJS())
506     {
507       JSON.setAjax(url);
508     }
509   }
510
511   public static Object parseJSON(InputStream response)
512           throws IOException, ParseException
513   {
514     if (isJS())
515     {
516       return JSON.parse(response);
517     }
518
519     BufferedReader br = null;
520     try
521     {
522       br = new BufferedReader(new InputStreamReader(response, "UTF-8"));
523       return new JSONParser().parse(br);
524     } finally
525     {
526       if (br != null)
527       {
528         try
529         {
530           br.close();
531         } catch (IOException e)
532         {
533           // ignore
534         }
535       }
536     }
537   }
538
539   public static Object parseJSON(String json) throws ParseException
540   {
541     return (isJS() ? JSON.parse(json)
542             : new JSONParser().parse(json));
543   }
544
545   public static Object parseJSON(Reader r)
546           throws IOException, ParseException
547   {
548     if (r == null)
549     {
550       return null;
551     }
552
553     if (!isJS())
554     {
555       return new JSONParser().parse(r);
556     }
557     // Using a file reader is not currently supported in SwingJS JavaScript
558
559     if (r instanceof FileReader)
560     {
561       throw new IOException(
562               "StringJS does not support FileReader parsing for JSON -- but it could...");
563     }
564     return JSON.parse(r);
565   }
566
567   /**
568    * Dump the input stream to an output file.
569    * 
570    * @param is
571    * @param outFile
572    * @throws IOException
573    *                       if the file cannot be created or there is a problem
574    *                       reading the input stream.
575    */
576   public static void streamToFile(InputStream is, File outFile)
577           throws IOException
578   {
579
580     if (isJS)
581     {
582       jsutil.setFileBytes(outFile, is);
583       return;
584     }
585
586     FileOutputStream fio = new FileOutputStream(outFile);
587     try
588     {
589       byte[] bb = new byte[32 * 1024];
590       int l;
591       while ((l = is.read(bb)) > 0)
592       {
593         fio.write(bb, 0, l);
594       }
595     } finally
596     {
597       fio.close();
598     }
599   }
600
601   /**
602    * Add a known domain that implements access-control-allow-origin:*
603    * 
604    * These should be reviewed periodically.
605    * 
606    * @param domain
607    *          for a service that is not allowing ajax
608    * 
609    * @author hansonr@stolaf.edu
610    * 
611    */
612   public static void addJ2SDirectDatabaseCall(String domain)
613   {
614
615     if (isJS)
616     {
617       jsutil.addDirectDatabaseCall(domain);
618
619       System.out.println(
620             "Platform adding known access-control-allow-origin * for domain "
621                     + domain);
622     }
623
624   }
625
626   public static void getURLCommandArguments()
627   {
628
629     /**
630      * Retrieve the first query field as command arguments to Jalview. Include
631      * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's __Info.args
632      * element to this value.
633      * 
634      * @j2sNative var a =
635      *            decodeURI((document.location.href.replace("&","?").split("?j2s")[0]
636      *            + "?").split("?")[1].split("#")[0]); a &&
637      *            (J2S.thisApplet.__Info.args = a.split(" "));
638      */
639
640   }
641
642   /**
643    * A (case sensitive) file path comparator that ignores the difference between /
644    * and \
645    * 
646    * @param path1
647    * @param path2
648    * @return
649    */
650   public static boolean pathEquals(String path1, String path2)
651   {
652     if (path1 == null)
653     {
654       return path2 == null;
655     }
656     if (path2 == null)
657     {
658       return false;
659     }
660     String p1 = path1.replace('\\', '/');
661     String p2 = path2.replace('\\', '/');
662     return p1.equals(p2);
663   }
664
665
666
667 ///////////// JAL-3253 Applet additions //////////////
668   
669   
670   /**
671    * Retrieve the object's embedded size from a div's style on a page if
672    * embedded in SwingJS.
673    * 
674    * @param frame
675    *          JFrame or JInternalFrame
676    * @param defaultWidth
677    *          use -1 to return null (no default size)
678    * @param defaultHeight
679    * @return the embedded dimensions or null (no default size or not embedded)
680    */
681   public static Dimension getDimIfEmbedded(Component frame,
682           int defaultWidth, int defaultHeight)
683   {
684     Dimension d = null;
685     if (isJS)
686     {
687       d = (Dimension) getEmbeddedAttribute(frame, "dim");
688     }
689     return (d == null && defaultWidth >= 0
690             ? new Dimension(defaultWidth, defaultHeight)
691             : d);
692
693   }
694
695   public static Regex newRegex(String searchString, String replaceString)
696   {
697     ensureRegex();
698     return (replaceString == null ? new Regex(searchString)
699             : new Regex(searchString, replaceString));
700   }
701
702   public static Regex newRegexPerl(String code)
703   {
704     ensureRegex();
705     return Regex.perlCode(code);
706   }
707
708   /**
709    * Initialize Java debug logging. A representative sample -- adapt as desired.
710    */
711   public static void startJavaLogging()
712   {
713     /**
714      * @j2sIgnore
715      */
716     {
717       logClass("java.awt.EventDispatchThread", "java.awt.EventQueue",
718               "java.awt.Component", "java.awt.focus.Component",
719               "java.awt.event.Component",
720               "java.awt.focus.DefaultKeyboardFocusManager");
721     }
722   }
723
724   /**
725    * Initiate Java logging for a given class. Only for Java, not JavaScript;
726    * Allows debugging of complex event processing.
727    * 
728    * @param className
729    */
730   public static void logClass(String... classNames)
731   {
732     /**
733      * @j2sIgnore
734      * 
735      * 
736      */
737     {
738       Logger rootLogger = Logger.getLogger("");
739       rootLogger.setLevel(Level.ALL);
740       ConsoleHandler consoleHandler = new ConsoleHandler();
741       consoleHandler.setLevel(Level.ALL);
742       for (int i = classNames.length; --i >= 0;)
743       {
744         Logger logger = Logger.getLogger(classNames[i]);
745         logger.setLevel(Level.ALL);
746         logger.addHandler(consoleHandler);
747       }
748     }
749   }
750
751
752   /**
753    * load a resource -- probably a core file -- if and only if a particular
754    * class has not been instantialized. We use a String here because if we used
755    * a .class object, that reference itself would simply load the class, and we
756    * want the core package to include that as well.
757    * 
758    * @param resourcePath
759    * @param className
760    */
761   public static void loadStaticResource(String resourcePath,
762           String className)
763   {
764     if (isJS)
765     {
766       jsutil.loadResourceIfClassUnknown(resourcePath, className);
767     }
768   }
769
770   public static void ensureRegex()
771   {
772     if (isJS)
773     {
774       loadStaticResource("core/core_stevesoft.z.js",
775               "com.stevesoft.pat.Regex");
776     }
777   }
778
779   /**
780    * Set the "app" property of the HTML5 applet object, for example,
781    * "testApplet.app", to point to the Jalview instance. This will be the object
782    * that page developers use that is similar to the original Java applet object
783    * that was accessed via LiveConnect.
784    * 
785    * @param j
786    */
787   public static void setAppClass(Object j)
788   {
789     if (isJS)
790     {
791       jsutil.setAppletAttribute("app", j);
792     }
793   }
794
795   /**
796    *
797    * If this frame ia embedded in a web page, return a known type.
798    * 
799    * @param frame
800    *          a JFrame or JInternalFrame
801    * @param type
802    * @return null if frame is not embedded.
803    */
804   public static Object getEmbeddedAttribute(Component frame, String type)
805   {
806     return (isJS ? jsutil.getEmbeddedAttribute(frame, type) : null);
807   }
808
809   public static void stackTrace()
810   {
811     try
812     {
813       throw new NullPointerException();
814     } catch (Exception e)
815     {
816       e.printStackTrace();
817     }
818
819   }
820
821   
822
823 }