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