99c82a48fed22ad2d58e98d9214dd98b0ea748e8
[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
40 import javax.swing.SwingUtilities;
41
42 import org.json.simple.parser.JSONParser;
43 import org.json.simple.parser.ParseException;
44
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.Set;
48 import java.util.logging.ConsoleHandler;
49 import java.util.logging.Level;
50 import java.util.logging.Logger;
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   
335   ////// jsutil additions, simplifications
336   
337   public static void cacheFileData(String path, Object data)
338   {
339     if (isJS())
340     {
341       jsutil.cachePathData(path, data);
342     }
343   }
344
345   public static void cacheFileData(File file)
346   {
347     if (isJS())
348     {
349       byte[] bytes = getFileBytes(file);
350       if (bytes != null)
351       {
352         cacheFileData(file.toString(), bytes);
353       }
354     }
355   }
356
357   public static byte[] getFileBytes(File f)
358   {
359     return (isJS() && f != null ? jsutil.getBytes(f) : null);
360   }
361
362   public static byte[] getFileAsBytes(String fileStr)
363   {
364     byte[] bytes = (isJS() && fileStr != null
365             ? (byte[]) jsutil.getFile(fileStr, false)
366             : null);
367     if (bytes != null)
368     {
369       cacheFileData(fileStr, bytes);
370     }
371     return bytes;
372   }
373
374   public static String getFileAsString(String url)
375   {
376     String ret = null;
377     if (isJS())
378     {
379       ret = (String) jsutil.getFile(url, true);
380       if (ret != null)
381       {
382         cacheFileData(url, ret);
383       }
384     }
385     return ret;
386   }
387
388   public static boolean setFileBytes(File f, String urlstring)
389   {
390     if (!isJS())
391     {
392       return false;
393     }
394     byte[] bytes = getFileAsBytes(urlstring);
395     boolean ok = false;
396     try
397     {
398       jsutil.setFileBytes(f, bytes);
399     } catch (Throwable t)
400     {
401       System.out.println("Platform.setFileBytes failed: " + t);
402     }
403     return ok;
404   }
405
406   public static void addJ2SBinaryType(String ext)
407   {
408     if (isJS())
409     {
410       jsutil.addBinaryFileType(ext);
411     }
412   }
413
414   /**
415    * Encode the URI using JavaScript encodeURIComponent
416    * 
417    * @param value
418    * @return encoded value
419    */
420   public static String encodeURI(String value)
421   {
422     /**
423      * @j2sNative value = encodeURIComponent(value);
424      */
425     return value;
426   }
427
428   /**
429    * Open the URL using a simple window call if this is JavaScript
430    * 
431    * @param url
432    * @return true if window has been opened
433    */
434   public static boolean openURL(String url) throws IOException
435   {
436     if (!isJS())
437     {
438       BrowserLauncher.openURL(url);
439       return false;
440     }
441     /**
442      * @j2sNative
443      * 
444      * 
445      *            window.open(url);
446      */
447     return true;
448   }
449
450   public static String getUniqueAppletID()
451   {
452           // Caution -- null here means using current thread instead of a known component.
453           
454           return jsutil.getAppletForComponent(null)._getID();
455   }
456
457   /**
458    * Read the Info block for this applet.
459    * 
460    * @param prefix
461    *          "jalview_"
462    * @param p
463    * @return unique id for this applet
464    */
465   public static void readInfoProperties(String prefix, Properties p)
466   {
467     if (isJS())
468     {
469       jsutil.readInfoProperties(prefix, p);
470     }
471   }
472
473   public static void setAjaxJSON(URL url)
474   {
475     if (isJS())
476     {
477       JSON.setAjax(url);
478     }
479   }
480
481   public static Object parseJSON(InputStream response)
482           throws IOException, ParseException
483   {
484     if (isJS())
485     {
486       return JSON.parse(response);
487     }
488
489     BufferedReader br = null;
490     try
491     {
492       br = new BufferedReader(new InputStreamReader(response, "UTF-8"));
493       return new JSONParser().parse(br);
494     } finally
495     {
496       if (br != null)
497       {
498         try
499         {
500           br.close();
501         } catch (IOException e)
502         {
503           // ignore
504         }
505       }
506     }
507   }
508
509   public static Object parseJSON(String json) throws ParseException
510   {
511     return (isJS() ? JSON.parse(json)
512             : new JSONParser().parse(json));
513   }
514
515   public static Object parseJSON(Reader r)
516           throws IOException, ParseException
517   {
518     if (r == null)
519     {
520       return null;
521     }
522
523     if (!isJS())
524     {
525       return new JSONParser().parse(r);
526     }
527     // Using a file reader is not currently supported in SwingJS JavaScript
528
529     if (r instanceof FileReader)
530     {
531       throw new IOException(
532               "StringJS does not support FileReader parsing for JSON -- but it could...");
533     }
534     return JSON.parse(r);
535
536   }
537
538   /**
539    * Dump the input stream to an output file.
540    * 
541    * @param is
542    * @param outFile
543    * @throws IOException
544    *           if the file cannot be created or there is a problem reading the
545    *           input stream.
546    */
547   public static void streamToFile(InputStream is, File outFile)
548           throws IOException
549   {
550     if (isJS() && jsutil.streamToFile(is, outFile))
551     {
552       return;
553     }
554     FileOutputStream fio = new FileOutputStream(outFile);
555     try
556     {
557       byte[] bb = new byte[32 * 1024];
558       int l;
559       while ((l = is.read(bb)) > 0)
560       {
561         fio.write(bb, 0, l);
562       }
563     } finally
564     {
565       fio.close();
566     }
567   }
568
569   /**
570    * Add a known domain that implements access-control-allow-origin:*
571    * 
572    * These should be reviewed periodically.
573    * 
574    * @param domain
575    *          for a service that is not allowing ajax
576    * 
577    * @author hansonr@stolaf.edu
578    * 
579    */
580   public static void addJ2SDirectDatabaseCall(String domain)
581   {
582
583     if (isJS())
584     {
585       jsutil.addDirectDatabaseCall(domain);
586     }
587   }
588
589   /**
590    * Retrieve the first query field as command arguments to Jalview. Include
591    * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's __Info.args
592    * element to this value.
593    */
594
595   @SuppressWarnings("unused")
596   public static void getURLCommandArguments()
597   {
598     if (!isJS())
599     {
600       return;
601     }
602     String[] args = null;
603     /**
604      * @j2sNative args =
605      *            decodeURI((document.location.href.replace("&","?").split("?j2s")[0]
606      *            + "?").split("?")[1].split("#")[0]); args && (args =
607      *            args.split(" "));
608      */
609     if (args != null)
610     {
611       jsutil.setAppletInfo("args", args);
612     }
613
614   }
615
616
617   /**
618    * A (case sensitive) file path comparator that ignores the difference between /
619    * and \
620    * 
621    * 
622    * @param path1
623    * @param path2
624    * @return
625    */
626   public static boolean pathEquals(String path1, String path2)
627   {
628     if (path1 == null)
629     {
630       return path2 == null;
631     }
632     if (path2 == null)
633     {
634       return false;
635     }
636     String p1 = path1.replace('\\', '/');
637     String p2 = path2.replace('\\', '/');
638     return p1.equals(p2);
639   }
640   
641   ///// new methods from applet branch only
642   
643   
644   public static URL getDocumentBase()
645   {
646     return (isJS() ? jsutil.getDocumentBase() : null);
647   }
648
649   public static URL getCodeBase()
650   {
651     return (isJS() ? jsutil.getCodeBase() : null);
652   }
653
654   public static void ensureJmol()
655   {
656     if (isJS())
657     {
658       jsutil.loadResourceIfClassUnknown("core/core_jvjmol.z.js",
659               "org.jmol.viewer.Viewer");
660     }
661   }
662
663   public static void ensureRegex()
664   {
665     if (isJS())
666     {
667     jsutil.loadResourceIfClassUnknown("core/core_stevesoft.z.js",
668             "com.stevesoft.pat.Regex");
669     }
670   }
671
672   public static Regex newRegex(String searchString, String replaceString)
673   {
674     ensureRegex();
675     return (replaceString == null ? new Regex(searchString)
676             : new Regex(searchString, replaceString));
677   }
678
679   public static Regex newRegexPerl(String code)
680   {
681     ensureRegex();
682     return Regex.perlCode(code);
683   }
684
685   /**
686    * Initialize Java debug logging. A representative sample -- adapt as desired.
687    */
688   public static void startJavaLogging()
689   {
690     /**
691      * @j2sIgnore
692      */
693     {
694       logClass("java.awt.EventDispatchThread", "java.awt.EventQueue",
695               "java.awt.Component", "java.awt.focus.Component",
696               "java.awt.event.Component",
697               "java.awt.focus.DefaultKeyboardFocusManager");
698     }
699   }
700
701   /**
702    * Initiate Java logging for a given class. Only for Java, not JavaScript;
703    * Allows debugging of complex event processing.
704    * 
705    * @param className
706    */
707   public static void logClass(String... classNames)
708   {
709     /**
710      * @j2sIgnore
711      * 
712      * 
713      */
714     {
715       Logger rootLogger = Logger.getLogger("");
716       rootLogger.setLevel(Level.ALL);
717       ConsoleHandler consoleHandler = new ConsoleHandler();
718       consoleHandler.setLevel(Level.ALL);
719       for (int i = classNames.length; --i >= 0;)
720       {
721         Logger logger = Logger.getLogger(classNames[i]);
722         logger.setLevel(Level.ALL);
723         logger.addHandler(consoleHandler);
724       }
725     }
726   }
727
728   /**
729    * Set the "app" property of the HTML5 applet object, for example,
730    * "testApplet.app", to point to the Jalview instance. This will be the object
731    * that page developers use that is similar to the original Java applet object
732    * that was accessed via LiveConnect.
733    * 
734    * @param app
735    */
736   public static void setAppClass(Object app)
737   {
738     if (isJS())
739     {
740       jsutil.setAppletAttribute("app", app);
741     }
742   }
743
744   /**
745    * Retrieve the object's embedded size from a div's style on a page if
746    * embedded in SwingJS.
747    * 
748    * @param frame
749    *          JFrame or JInternalFrame
750    * @param defaultWidth
751    *          use -1 to return null (no default size)
752    * @param defaultHeight
753    * @return the embedded dimensions or null (no default size or not embedded)
754    */
755   public static Dimension getDimIfEmbedded(Component frame,
756           int defaultWidth, int defaultHeight)
757   {
758     Dimension d = (Dimension) getEmbeddedAttribute(frame, "dim");
759     return (d == null && defaultWidth >= 0
760             ? new Dimension(defaultWidth, defaultHeight)
761             : d);
762   }
763
764   /**
765    *
766    * If this frame Is this frame embedded in a web page, return a known type.
767    * 
768    * @param frame
769    *          a JFrame or JInternalFrame
770    * @param type
771    * @return null if frame is not embedded.
772    */
773   public static Object getEmbeddedAttribute(Component frame, String type)
774   {
775     return (isJS() ? jsutil.getEmbeddedAttribute(frame, type) : null);
776   }
777
778   /**
779    * Only called for JavaScript.
780    * 
781    * @return Map for static singleton classes unique to a given applet
782    */
783   public static HashMap<?, ?> getJSSingletons()
784   {
785     return (isJS() ? jsutil.getJSContext("jssingletons") : null);
786   }
787
788   /**
789    * By designating initialCapacity and loadFactor, we tell SwingJS to use a
790    * standard (slower) Java HashMap to back this HashSet, thus providing exactly
791    * the same iterator order (until a new Java version changes it!)
792    * 
793    * @return a standard Java HashSet
794    */
795   public static Set<String> getJavaOrderedHashSet()
796   {
797     return new HashSet<>(16, 0.75f);
798   }
799
800   /**
801    * Switch the flag in SwingJS to use or not use the JavaScript Map object in
802    * any Hashtable, HashMap, or HashSet. Default is enabled.
803    * 
804    * For testing purposes only.
805    * 
806    */
807   public static boolean setJavaScriptMapObjectEnabled(boolean enabled)
808   {
809     if (!isJS())
810     {
811       return false;
812     }
813     jsutil.setJavaScriptMapObjectEnabled(enabled);
814     HashSet<String> hs = new HashSet<>();
815     // Java hash table iterator in HashMap will return "one" before "two"
816     // because of its hash code;
817     // JavaScript Map object will return "two" first because it was added first.
818     hs.add("two");
819     hs.add("one");
820     return (hs.iterator().next() == (enabled ? "two" : "one"));
821   }
822   
823
824   public static void stackTrace()
825   {
826     new NullPointerException("testing only").printStackTrace();
827   }
828
829
830   /**
831    * escape a string according to the local platform's escape character
832    * 
833    * @param file
834    * @return escaped file
835    */
836   public static String escapeString(String file)
837   {
838     StringBuffer f = new StringBuffer();
839     int p = 0, lastp = 0;
840     while ((p = file.indexOf('\\', lastp)) > -1)
841     {
842       f.append(file.subSequence(lastp, p));
843       f.append("\\\\");
844       lastp = p + 1;
845     }
846     f.append(file.substring(lastp));
847     return f.toString();
848   }
849
850 }