JAL-4059 New namespaced query string parameters.
[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.Toolkit;
24 import java.awt.event.MouseEvent;
25 import java.io.BufferedReader;
26 import java.io.File;
27 import java.io.FileOutputStream;
28 import java.io.FileReader;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InputStreamReader;
32 import java.io.Reader;
33 import java.net.URL;
34 import java.util.Properties;
35
36 import javax.swing.SwingUtilities;
37
38 import org.json.simple.parser.JSONParser;
39 import org.json.simple.parser.ParseException;
40
41 import jalview.bin.argparser.ArgParser;
42 import jalview.javascript.json.JSON;
43
44 /**
45  * System platform information used by Applet and Application
46  * 
47  * @author Jim Procter
48  */
49 public class Platform
50 {
51
52   private static boolean isJS = /** @j2sNative true || */
53           false;
54
55   private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
56           isWin = null, isLinux = null;
57
58   private static Boolean isHeadless = null;
59
60   // If launched from CLI with launcher script then -DCOLUMNWIDTH is set
61   private static final int CONSOLEWIDTH;
62
63   private static final String CONSOLEWIDTHPROPERTY = "CONSOLEWIDTH";
64
65   static
66   {
67     int cw = 80;
68     if (System.getProperty(CONSOLEWIDTHPROPERTY) != null
69             && System.getProperty(CONSOLEWIDTHPROPERTY).length() > 0)
70     {
71       try
72       {
73         cw = Integer.parseInt(System.getProperty(CONSOLEWIDTHPROPERTY));
74       } catch (NumberFormatException e)
75       {
76       }
77     }
78     CONSOLEWIDTH = cw;
79   }
80
81   /**
82    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
83    * 
84    * @return
85    */
86   public static boolean isMac()
87   {
88     return (isMac == null
89             ? (isMac = (System.getProperty("os.name").indexOf("Mac") >= 0))
90             : isMac);
91   }
92
93   /**
94    * added to group mouse events into Windows and nonWindows (mac, unix, linux)
95    * 
96    * @return
97    */
98   public static boolean isWin()
99   {
100     return (isWin == null
101             ? (isWin = (System.getProperty("os.name").indexOf("Win") >= 0))
102             : isWin);
103   }
104
105   /**
106    * added to check LaF for Linux
107    * 
108    * @return
109    */
110   public static boolean isLinux()
111   {
112     return (isLinux == null
113             ? (isLinux = (System.getProperty("os.name")
114                     .indexOf("Linux") >= 0))
115             : isLinux);
116   }
117
118   /**
119    * 
120    * @return true if HTML5 JavaScript
121    */
122   public static boolean isJS()
123   {
124     return isJS;
125   }
126
127   /**
128    * sorry folks - Macs really are different
129    * 
130    * BH: disabled for SwingJS -- will need to check key-press issues
131    * 
132    * @return true if we do things in a special way.
133    */
134   public static boolean isAMacAndNotJS()
135   {
136     return (isNoJSMac == null ? (isNoJSMac = !isJS && isMac()) : isNoJSMac);
137   }
138
139   /**
140    * Check if we are on a Microsoft plaform...
141    * 
142    * @return true if we have to cope with another platform variation
143    */
144   public static boolean isWindowsAndNotJS()
145   {
146     return (isNoJSWin == null ? (isNoJSWin = !isJS && isWin()) : isNoJSWin);
147   }
148
149   /**
150    * 
151    * @return true if we are running in non-interactive no UI mode
152    */
153   public static boolean isHeadless()
154   {
155     if (isHeadless == null)
156     {
157       isHeadless = "true".equals(System.getProperty("java.awt.headless"));
158     }
159     return isHeadless;
160   }
161
162   /**
163    * 
164    * @return nominal maximum command line length for this platform
165    */
166   public static int getMaxCommandLineLength()
167   {
168     // TODO: determine nominal limits for most platforms.
169     return 2046; // this is the max length for a windows NT system.
170   }
171
172   /**
173    * Answers the input with every backslash replaced with a double backslash (an
174    * 'escaped' single backslash)
175    * 
176    * @param s
177    * @return
178    */
179   public static String escapeBackslashes(String s)
180   {
181     return s == null ? null : s.replace("\\", "\\\\");
182   }
183
184   /**
185    * Answers true if the mouse event has Meta-down (Command key on Mac) or
186    * Ctrl-down (on other o/s). Note this answers _false_ if the Ctrl key is
187    * pressed instead of the Meta/Cmd key on Mac. To test for Ctrl-pressed on
188    * Mac, you can use e.isPopupTrigger().
189    * 
190    * @param e
191    * @return
192    */
193   public static boolean isControlDown(MouseEvent e)
194   {
195     return isControlDown(e, isMac());
196   }
197
198   /**
199    * Overloaded version of method (to allow unit testing)
200    * 
201    * @param e
202    * @param aMac
203    * @return
204    */
205   protected static boolean isControlDown(MouseEvent e, boolean aMac)
206   {
207     if (!aMac)
208     {
209       return e.isControlDown();
210
211       // Jalview 2.11 code below: above is as amended for JalviewJS
212       // /*
213       // * answer false for right mouse button
214       // */
215       // if (e.isPopupTrigger())
216       // {
217       // return false;
218       // }
219       // return
220       // (jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx() //
221       // .getMenuShortcutKeyMaskEx()
222       // & jalview.util.ShortcutKeyMaskExWrapper
223       // .getModifiersEx(e)) != 0; // getModifiers()) != 0;
224     }
225     // answer false for right mouse button
226     // shortcut key will be META for a Mac
227     return !e.isPopupTrigger()
228             && (Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
229                     & e.getModifiers()) != 0;
230     // could we use e.isMetaDown() here?
231   }
232
233   // BH: I don't know about that previous method. Here is what SwingJS uses.
234   // Notice the distinction in mouse events. (BUTTON3_MASK == META)
235   //
236   // private static boolean isPopupTrigger(int id, int mods, boolean isWin) {
237   // boolean rt = ((mods & InputEvent.BUTTON3_MASK) != 0);
238   // if (isWin) {
239   // if (id != MouseEvent.MOUSE_RELEASED)
240   // return false;
241   ////
242   //// // Oddly, Windows returns InputEvent.META_DOWN_MASK on release, though
243   //// // BUTTON3_DOWN_MASK for pressed. So here we just accept both.
244   ////
245   //// actually, we can use XXX_MASK, not XXX_DOWN_MASK and avoid this issue,
246   // because
247   //// J2S adds the appropriate extended (0x3FC0) and simple (0x3F) modifiers.
248   ////
249   // return rt;
250   // } else {
251   // // mac, linux, unix
252   // if (id != MouseEvent.MOUSE_PRESSED)
253   // return false;
254   // boolean lt = ((mods & InputEvent.BUTTON1_MASK) != 0);
255   // boolean ctrl = ((mods & InputEvent.CTRL_MASK) != 0);
256   // return rt || (ctrl && lt);
257   // }
258   // }
259   //
260
261   /**
262    * Windows (not Mac, Linux, or Unix) and right button to test for the
263    * right-mouse pressed event in Windows that would have opened a menu or a
264    * Mac.
265    * 
266    * @param e
267    * @return
268    */
269   public static boolean isWinRightButton(MouseEvent e)
270   {
271     // was !isAMac(), but that is true also for Linux and Unix and JS,
272
273     return isWin() && SwingUtilities.isRightMouseButton(e);
274   }
275
276   /**
277    * Windows (not Mac, Linux, or Unix) and middle button -- for mouse wheeling
278    * without pressing the button.
279    * 
280    * @param e
281    * @return
282    */
283   public static boolean isWinMiddleButton(MouseEvent e)
284   {
285     // was !isAMac(), but that is true also for Linux and Unix and JS
286     return isWin() && SwingUtilities.isMiddleMouseButton(e);
287   }
288
289   public static boolean allowMnemonics()
290   {
291     return !isMac();
292   }
293
294   public final static int TIME_RESET = 0;
295
296   public final static int TIME_MARK = 1;
297
298   public static final int TIME_SET = 2;
299
300   public static final int TIME_GET = 3;
301
302   public static long time, mark, set, duration;
303
304   public static void timeCheck(String msg, int mode)
305   {
306     long t = System.currentTimeMillis();
307     switch (mode)
308     {
309     case TIME_RESET:
310       time = mark = t;
311       if (msg != null)
312       {
313         jalview.bin.Console.errPrintln("Platform: timer reset\t\t\t" + msg);
314       }
315       break;
316     case TIME_MARK:
317       if (set > 0)
318       {
319         duration += (t - set);
320       }
321       else
322       {
323         if (time == 0)
324         {
325           time = mark = t;
326         }
327         if (msg != null)
328         {
329           jalview.bin.Console.errPrintln(
330                   "Platform: timer mark\t" + ((t - time) / 1000f) + "\t"
331                           + ((t - mark) / 1000f) + "\t" + msg);
332         }
333         mark = t;
334       }
335       break;
336     case TIME_SET:
337       set = t;
338       break;
339     case TIME_GET:
340       if (msg != null)
341       {
342         jalview.bin.Console
343                 .errPrintln("Platform: timer dur\t" + ((t - time) / 1000f)
344                         + "\t" + ((duration) / 1000f) + "\t" + msg);
345       }
346       set = 0;
347       break;
348     }
349   }
350
351   public static void cacheFileData(String path, Object data)
352   {
353     if (!isJS() || data == null)
354     {
355       return;
356     }
357     /**
358      * @j2sNative
359      * 
360      *            swingjs.JSUtil.cacheFileData$S$O(path, data);
361      * 
362      */
363   }
364
365   public static void cacheFileData(File file)
366   {
367     byte[] data;
368     if (!isJS() || (data = Platform.getFileBytes(file)) == null)
369     {
370       return;
371     }
372     cacheFileData(file.toString(), data);
373   }
374
375   public static byte[] getFileBytes(File f)
376   {
377     return /** @j2sNative f && swingjs.JSUtil.getFileAsBytes$O(f) || */
378     null;
379   }
380
381   public static byte[] getFileAsBytes(String fileStr)
382   {
383     byte[] bytes = null;
384     // BH 2018 hack for no support for access-origin
385     /**
386      * @j2sNative bytes = swingjs.JSUtil.getFileAsBytes$O(fileStr)
387      */
388     cacheFileData(fileStr, bytes);
389     return bytes;
390   }
391
392   @SuppressWarnings("unused")
393   public static String getFileAsString(String url)
394   {
395     String ret = null;
396     /**
397      * @j2sNative
398      * 
399      *            ret = swingjs.JSUtil.getFileAsString$S(url);
400      * 
401      * 
402      */
403     cacheFileData(url, ret);
404     return ret;
405   }
406
407   public static boolean setFileBytes(File f, String urlstring)
408   {
409     if (!isJS())
410     {
411       return false;
412     }
413     @SuppressWarnings("unused")
414     byte[] bytes = getFileAsBytes(urlstring);
415     // TODO temporary doubling of ç§˜bytes and _bytes;
416     // just remove _bytes when new transpiler has been installed
417     /**
418      * @j2sNative f.\u79d8bytes = f._bytes = bytes;
419      */
420     return true;
421   }
422
423   public static void addJ2SBinaryType(String ext)
424   {
425     /**
426      * @j2sNative
427      * 
428      *            J2S._binaryTypes.push("." + ext + "?");
429      * 
430      */
431   }
432
433   /**
434    * Encode the URI using JavaScript encodeURIComponent
435    * 
436    * @param value
437    * @return encoded value
438    */
439   public static String encodeURI(String value)
440   {
441     /**
442      * @j2sNative value = encodeURIComponent(value);
443      */
444     return value;
445   }
446
447   /**
448    * Open the URL using a simple window call if this is JavaScript
449    * 
450    * @param url
451    * @return true if window has been opened
452    */
453   public static boolean openURL(String url)
454   {
455     if (!isJS())
456     {
457       return false;
458     }
459     /**
460      * @j2sNative
461      * 
462      * 
463      *            window.open(url);
464      */
465     return true;
466   }
467
468   public static String getUniqueAppletID()
469   {
470     /**
471      * @j2sNative return swingjs.JSUtil.getApplet$()._uniqueId;
472      *
473      */
474     return null;
475
476   }
477
478   /**
479    * Read the Info block for this applet.
480    * 
481    * @param prefix
482    *          "jalview_"
483    * @param p
484    * @return unique id for this applet
485    */
486   public static void readInfoProperties(String prefix, Properties p)
487   {
488     if (!isJS())
489     {
490       return;
491     }
492     String id = getUniqueAppletID();
493     String key = "", value = "";
494     /**
495      * @j2sNative var info = swingjs.JSUtil.getApplet$().__Info || {}; for (var
496      *            key in info) { if (key.indexOf(prefix) == 0) { value = "" +
497      *            info[key];
498      */
499
500     jalview.bin.Console.outPrintln(
501             "Platform id=" + id + " reading Info." + key + " = " + value);
502     p.put(id + "_" + key, value);
503
504     /**
505      * @j2sNative
506      * 
507      * 
508      *            } }
509      */
510   }
511
512   public static void setAjaxJSON(URL url)
513   {
514     if (isJS())
515     {
516       JSON.setAjax(url);
517     }
518   }
519
520   public static Object parseJSON(InputStream response)
521           throws IOException, ParseException
522   {
523     if (isJS())
524     {
525       return JSON.parse(response);
526     }
527
528     BufferedReader br = null;
529     try
530     {
531       br = new BufferedReader(new InputStreamReader(response, "UTF-8"));
532       return new JSONParser().parse(br);
533     } finally
534     {
535       if (br != null)
536       {
537         try
538         {
539           br.close();
540         } catch (IOException e)
541         {
542           // ignore
543         }
544       }
545     }
546   }
547
548   public static Object parseJSON(String json) throws ParseException
549   {
550     return (isJS() ? JSON.parse(json) : new JSONParser().parse(json));
551   }
552
553   public static Object parseJSON(Reader r)
554           throws IOException, ParseException
555   {
556     if (r == null)
557     {
558       return null;
559     }
560
561     if (!isJS())
562     {
563       return new JSONParser().parse(r);
564     }
565     // Using a file reader is not currently supported in SwingJS JavaScript
566
567     if (r instanceof FileReader)
568     {
569       throw new IOException(
570               "StringJS does not support FileReader parsing for JSON -- but it could...");
571     }
572     return JSON.parse(r);
573
574   }
575
576   /**
577    * Dump the input stream to an output file.
578    * 
579    * @param is
580    * @param outFile
581    * @throws IOException
582    *           if the file cannot be created or there is a problem reading the
583    *           input stream.
584    */
585   public static void streamToFile(InputStream is, File outFile)
586           throws IOException
587   {
588     if (isJS() && /**
589                    * @j2sNative outFile.setBytes$O && outFile.setBytes$O(is) &&
590                    */
591             true)
592     {
593       return;
594     }
595     FileOutputStream fio = new FileOutputStream(outFile);
596     try
597     {
598       byte[] bb = new byte[32 * 1024];
599       int l;
600       while ((l = is.read(bb)) > 0)
601       {
602         fio.write(bb, 0, l);
603       }
604     } finally
605     {
606       fio.close();
607     }
608   }
609
610   /**
611    * Add a known domain that implements access-control-allow-origin:*
612    * 
613    * These should be reviewed periodically.
614    * 
615    * @param domain
616    *          for a service that is not allowing ajax
617    * 
618    * @author hansonr@stolaf.edu
619    * 
620    */
621   public static void addJ2SDirectDatabaseCall(String domain)
622   {
623
624     if (isJS())
625     {
626       jalview.bin.Console.outPrintln(
627               "Platform adding known access-control-allow-origin * for domain "
628                       + domain);
629       /**
630        * @j2sNative
631        * 
632        *            J2S.addDirectDatabaseCall(domain);
633        */
634     }
635
636   }
637
638   public static void getURLCommandArguments()
639   {
640
641     // setting ArgParser.ignoreNonStringValues allows non-string args to be
642     // set with, e.g., --wrap=hello
643     // which might be necessary for a querystring, plus we don't have access
644     // to Arg.hasOption(Opt.STRING)
645     if (Platform.isJS())
646     {
647       ArgParser.setIgnoreNonStringValues(true);
648     }
649     try
650     {
651       // extra spaces between lines of javascript to avoid eclipse comment
652       // munging into one line
653
654       /**
655        * Retrieve the first query field as command arguments to Jalview. Include
656        * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's
657        * __Info.args element to this value.
658        * 
659        * if a querystringnamespace has been given in Info={...}. Use this
660        * namespace to find arguments and values in the querystring parameters.
661        * Arguments that do not take a value do not need to have a value in the
662        * querystring. If they do they will be ignored. Note, this means you
663        * cannot do 'debug=false' instead of 'nodebug'. If querystringnamepsace
664        * is an empty string ("") then no colon (":") will be expected.
665        * 
666        * if querystringnamespace is not defined then use the old style single
667        * first parameter for arguments
668        *
669        * @j2sNative var querystringnamespace =
670        *            J2S.thisApplet.__Info.querystringnamespace;
671        * 
672        *            if (querystringnamespace === undefined)
673        * 
674        *            {
675        * 
676        *            System.out.println("No querystringnamespace given");
677        * 
678        *            var a =
679        *            decodeURI((document.location.href.replace("&","?").split("?j2s")[0]
680        *            + "?").split("?")[1].split("#")[0]);
681        * 
682        *            a && (System.out.println("URL arguments detected were "+a))
683        *            && (J2S.thisApplet.__Info.urlargs = a.split(" "));
684        * 
685        *            (!J2S.thisApplet.__Info.args || J2S.thisApplet.__Info.args
686        *            == "" || J2S.thisApplet.__Info.args == "??") &&
687        *            (J2S.thisApplet.__Info.args = a) && (System.out.println("URL
688        *            arguments were passed to J2S main."));
689        * 
690        *            }
691        * 
692        *            else // querystringnamespace is defined
693        * 
694        *            {
695        * 
696        *            var qsnsc = "";
697        * 
698        *            if (J2S.thisApplet.__Info.querystringnamespace) {
699        * 
700        *            qsnsc = J2S.thisApplet.__Info.querystringnamespace + ":";
701        * 
702        *            }
703        * 
704        *            System.out.println("querystringnamespace is '"+qsnsc+"'");
705        * 
706        *            var qsParams = new URLSearchParams(window.location.search);
707        * 
708        *            var qsargs = [];
709        * 
710        *            for (var param of qsParams) {
711        * 
712        *            var key = param[0];
713        * 
714        *            var val = param[1];
715        * 
716        *            if (key.startsWith(qsnsc)) {
717        * 
718        *            var arg = key.substring(qsnsc.length);
719        * 
720        *            qsargs.push("--" + arg + "=" + val);
721        * 
722        *            System.out.println("Setting arg '"+arg+"' to '"+val+"'");
723        * 
724        *            }
725        * 
726        *            }
727        * 
728        *            qsargs && (System.out.println("URL parameters detected were
729        *            "+qsargs.join(" "))) && (J2S.thisApplet.__Info.urlargs =
730        *            qsargs);
731        * 
732        *            (!J2S.thisApplet.__Info.args || J2S.thisApplet.__Info.args
733        *            == "" || J2S.thisApplet.__Info.args == "??") &&
734        *            (J2S.thisApplet.__Info.args = qsargs.join(" ")) &&
735        *            (System.out.println("URL parameters were passed to J2S
736        *            main."));
737        * 
738        *            }
739        */
740     } catch (Throwable t)
741     {
742       /**
743        * @j2sNative console.log("Problem looking for arguments");
744        *            console.log(t);
745        */
746     }
747   }
748
749   /**
750    * A (case sensitive) file path comparator that ignores the difference between
751    * / and \
752    * 
753    * @param path1
754    * @param path2
755    * @return
756    */
757   public static boolean pathEquals(String path1, String path2)
758   {
759     if (path1 == null)
760     {
761       return path2 == null;
762     }
763     if (path2 == null)
764     {
765       return false;
766     }
767     String p1 = path1.replace('\\', '/');
768     String p2 = path2.replace('\\', '/');
769     return p1.equals(p2);
770   }
771
772   /**
773    * If started on command line using launch script, return the console width
774    */
775   public static int consoleWidth()
776   {
777     return CONSOLEWIDTH;
778   }
779 }