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