2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
24 import java.io.IOException;
25 import java.lang.reflect.Constructor;
26 import java.lang.reflect.Field;
27 import java.lang.reflect.InvocationTargetException;
28 import java.lang.reflect.Method;
31 * BrowserLauncher is a class that provides one static method, openURL, which
32 * opens the default web browser for the current user of the system to the given
33 * URL. It may support other protocols depending on the system -- mailto, ftp,
34 * etc. -- but that has not been rigorously tested and is not guaranteed to
37 * Yes, this is platform-specific code, and yes, it may rely on classes on
38 * certain platforms that are not part of the standard JDK. What we're trying to
39 * do, though, is to take something that's frequently desirable but inherently
40 * platform-specific -- opening a default browser -- and allow programmers (you,
41 * for example) to do so without worrying about dropping into native code or
42 * doing anything else similarly evil.
44 * Anyway, this code is completely in Java and will run on all JDK 1.1-compliant
45 * systems without modification or a need for additional libraries. All classes
46 * that are required on certain platforms to allow this to run are dynamically
47 * loaded at runtime via reflection and, if not found, will not cause this to do
48 * anything other than returning an error when opening the browser.
50 * There are certain system requirements for this class, as it's running through
51 * Runtime.exec(), which is Java's way of making a native system call.
52 * Currently, this requires that a Macintosh have a Finder which supports the
53 * GURL event, which is true for Mac OS 8.0 and 8.1 systems that have the
54 * Internet Scripting AppleScript dictionary installed in the Scripting
55 * Additions folder in the Extensions folder (which is installed by default as
56 * far as I know under Mac OS 8.0 and 8.1), and for all Mac OS 8.5 and later
57 * systems. On Windows, it only runs under Win32 systems (Windows 95, 98, and NT
58 * 4.0, as well as later versions of all). On other systems, this drops back
59 * from the inherently platform-sensitive concept of a default browser and
60 * simply attempts to launch Netscape via a shell command.
62 * This code is Copyright 1999-2001 by Eric Albert (ejalbert\@cs.stanford.edu)
63 * and may be redistributed or modified in any form without restrictions as long
64 * as the portion of this comment from this paragraph through the end of the
65 * comment is not removed. The author requests that he be notified of any
66 * application, applet, or other binary that makes use of this code, but that's
67 * more out of curiosity than anything and is not required. This software
68 * includes no warranty. The author is not repsonsible for any loss of data or
69 * functionality or any adverse or unexpected effects of using this software.
72 * Steven Spencer, JavaWorld magazine
73 * (<a href="http://www.javaworld.com/javaworld/javatips/jw-javatip66.html">Java
75 * Thanks also to Ron B. Yeh, Eric Shapiro, Ben Engber, Paul Teitlebaum, Andrea
76 * Cantatore, Larry Barowski, Trevor Bedzek, Frank Miedrich, and Ron Rabakukk
78 * @author Eric Albert (<a href=
79 * "mailto:ejalbert@cs.stanford.edu">ejalbert@cs.stanford.edu</a>)
80 * @version 1.4b1 (Released June 20, 2001)
82 public class BrowserLauncher
85 * The Java virtual machine that we are running on. Actually, in most cases we
86 * only care about the operating system, but some operating systems require us
87 * to switch on the VM.
89 private static int jvm;
91 /** The browser for the system */
92 private static Object browser;
95 * Caches whether any classes, methods, and fields that are not part of the
96 * JDK and need to be dynamically loaded at runtime loaded successfully.
98 * Note that if this is <code>false</code>, <code>openURL()</code> will always
99 * return an IOException.
101 private static boolean loadedWithoutErrors;
103 /** The com.apple.mrj.MRJFileUtils class */
104 private static Class mrjFileUtilsClass;
106 /** The com.apple.mrj.MRJOSType class */
107 private static Class mrjOSTypeClass;
109 /** The com.apple.MacOS.AEDesc class */
110 private static Class aeDescClass;
112 /** The <init>(int) method of com.apple.MacOS.AETarget */
113 private static Constructor aeTargetConstructor;
115 /** The <init>(int, int, int) method of com.apple.MacOS.AppleEvent */
116 private static Constructor appleEventConstructor;
118 /** The <init>(String) method of com.apple.MacOS.AEDesc */
119 private static Constructor aeDescConstructor;
121 /** The findFolder method of com.apple.mrj.MRJFileUtils */
122 private static Method findFolder;
124 /** The getFileCreator method of com.apple.mrj.MRJFileUtils */
125 private static Method getFileCreator;
127 /** The getFileType method of com.apple.mrj.MRJFileUtils */
128 private static Method getFileType;
130 /** The openURL method of com.apple.mrj.MRJFileUtils */
131 private static Method openURL;
133 /** The makeOSType method of com.apple.MacOS.OSUtils */
134 private static Method makeOSType;
136 /** The putParameter method of com.apple.MacOS.AppleEvent */
137 private static Method putParameter;
139 /** The sendNoReply method of com.apple.MacOS.AppleEvent */
140 private static Method sendNoReply;
142 /** Actually an MRJOSType pointing to the System Folder on a Macintosh */
143 private static Object kSystemFolderType;
145 /** The keyDirectObject AppleEvent parameter type */
146 private static Integer keyDirectObject;
148 /** The kAutoGenerateReturnID AppleEvent code */
149 private static Integer kAutoGenerateReturnID;
151 /** The kAnyTransactionID AppleEvent code */
152 private static Integer kAnyTransactionID;
154 /** The linkage object required for JDirect 3 on Mac OS X. */
155 private static Object linkage;
157 /** The framework to reference on Mac OS X */
158 private static final String JDirect_MacOSX = "/System/Library/Frameworks/Carbon.framework/Frameworks/HIToolbox.framework/HIToolbox";
160 /** JVM constant for MRJ 2.0 */
161 private static final int MRJ_2_0 = 0;
163 /** JVM constant for MRJ 2.1 or later */
164 private static final int MRJ_2_1 = 1;
166 /** JVM constant for Java on Mac OS X 10.0 (MRJ 3.0) */
167 private static final int MRJ_3_0 = 3;
169 /** JVM constant for MRJ 3.1 */
170 private static final int MRJ_3_1 = 4;
172 /** JVM constant for any Windows NT JVM */
173 private static final int WINDOWS_NT = 5;
175 /** JVM constant for any Windows 9x JVM */
176 private static final int WINDOWS_9x = 6;
178 /** JVM constant for any other platform */
179 private static final int OTHER = -1;
182 * The file type of the Finder on a Macintosh. Hardcoding "Finder" would keep
183 * non-U.S. English systems from working properly.
185 private static final String FINDER_TYPE = "FNDR";
188 * The creator code of the Finder on a Macintosh, which is needed to send
189 * AppleEvents to the application.
191 private static final String FINDER_CREATOR = "MACS";
193 /** The name for the AppleEvent type corresponding to a GetURL event. */
194 private static final String GURL_EVENT = "GURL";
197 * The first parameter that needs to be passed into Runtime.exec() to open the
198 * default web browser on Windows.
200 private static final String FIRST_WINDOWS_PARAMETER = "/c";
202 /** The second parameter for Runtime.exec() on Windows. */
203 private static final String SECOND_WINDOWS_PARAMETER = "start";
206 * The third parameter for Runtime.exec() on Windows. This is a "title"
207 * parameter that the command line expects. Setting this parameter allows URLs
208 * containing spaces to work.
210 private static final String THIRD_WINDOWS_PARAMETER = "\"\"";
213 * The shell parameters for Netscape that opens a given URL in an already-open
214 * copy of Netscape on many command-line systems.
216 private static final String NETSCAPE_REMOTE_PARAMETER = "-remote";
218 private static final String NETSCAPE_OPEN_PARAMETER_START = "openURL(";
220 private static final String NETSCAPE_OPEN_NEW_WINDOW = ", new-window";
222 private static final String NETSCAPE_OPEN_PARAMETER_END = ")";
225 * The message from any exception thrown throughout the initialization
228 private static String errorMessage;
231 * An initialization block that determines the operating system and loads the
232 * necessary runtime data.
237 loadedWithoutErrors = true;
245 String osName = System.getProperty("os.name");
247 if (osName.startsWith("Mac OS"))
249 String mrjVersion = System.getProperty("mrj.version");
250 String majorMRJVersion;
251 if (mrjVersion == null)
253 // must be on some later build with mrj support
254 majorMRJVersion = "3.1";
258 majorMRJVersion = mrjVersion.substring(0, 3);
263 double version = Double.valueOf(majorMRJVersion).doubleValue();
269 else if ((version >= 2.1) && (version < 3))
271 // Assume that all 2.x versions of MRJ work the same. MRJ 2.1 actually
272 // works via Runtime.exec() and 2.2 supports that but has an openURL()
274 // as well that we currently ignore.
277 else if (version == 3.0)
281 else if (version >= 3.1)
283 // Assume that all 3.1 and later versions of MRJ work the same.
288 loadedWithoutErrors = false;
289 errorMessage = "Unsupported MRJ version: " + version;
291 } catch (NumberFormatException nfe)
293 loadedWithoutErrors = false;
294 errorMessage = "Invalid MRJ version: " + mrjVersion;
297 else if (osName.startsWith("Windows"))
299 if (osName.indexOf("9") != -1)
313 if (loadedWithoutErrors)
314 { // if we haven't hit any errors yet
315 loadedWithoutErrors = loadClasses();
321 * This class should be never be instantiated; this just ensures so.
323 private BrowserLauncher()
328 * Called by a static initializer to load any classes, fields, and methods
329 * required at runtime to locate the user's web browser.
331 * @return <code>true</code> if all intialization succeeded <code>false</code>
332 * if any portion of the initialization failed
334 private static boolean loadClasses()
348 Class aeTargetClass = Class.forName("com.apple.MacOS.AETarget");
349 Class osUtilsClass = Class.forName("com.apple.MacOS.OSUtils");
350 Class appleEventClass = Class.forName("com.apple.MacOS.AppleEvent");
351 Class aeClass = Class.forName("com.apple.MacOS.ae");
352 aeDescClass = Class.forName("com.apple.MacOS.AEDesc");
354 aeTargetConstructor = aeTargetClass
355 .getDeclaredConstructor(new Class[]
357 appleEventConstructor = appleEventClass
358 .getDeclaredConstructor(new Class[]
359 { int.class, int.class, aeTargetClass, int.class,
361 aeDescConstructor = aeDescClass
362 .getDeclaredConstructor(new Class[]
365 makeOSType = osUtilsClass.getDeclaredMethod("makeOSType",
368 putParameter = appleEventClass.getDeclaredMethod("putParameter",
370 { int.class, aeDescClass });
371 sendNoReply = appleEventClass.getDeclaredMethod("sendNoReply",
374 Field keyDirectObjectField = aeClass
375 .getDeclaredField("keyDirectObject");
376 keyDirectObject = (Integer) keyDirectObjectField.get(null);
378 Field autoGenerateReturnIDField = appleEventClass
379 .getDeclaredField("kAutoGenerateReturnID");
380 kAutoGenerateReturnID = (Integer) autoGenerateReturnIDField
383 Field anyTransactionIDField = appleEventClass
384 .getDeclaredField("kAnyTransactionID");
385 kAnyTransactionID = (Integer) anyTransactionIDField.get(null);
386 } catch (ClassNotFoundException cnfe)
388 errorMessage = cnfe.getMessage();
391 } catch (NoSuchMethodException nsme)
393 errorMessage = nsme.getMessage();
396 } catch (NoSuchFieldException nsfe)
398 errorMessage = nsfe.getMessage();
401 } catch (IllegalAccessException iae)
403 errorMessage = iae.getMessage();
414 mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils");
415 mrjOSTypeClass = Class.forName("com.apple.mrj.MRJOSType");
417 Field systemFolderField = mrjFileUtilsClass
418 .getDeclaredField("kSystemFolderType");
419 kSystemFolderType = systemFolderField.get(null);
420 findFolder = mrjFileUtilsClass.getDeclaredMethod("findFolder",
423 getFileCreator = mrjFileUtilsClass
424 .getDeclaredMethod("getFileCreator", new Class[]
426 getFileType = mrjFileUtilsClass.getDeclaredMethod("getFileType",
429 } catch (ClassNotFoundException cnfe)
431 errorMessage = cnfe.getMessage();
434 } catch (NoSuchFieldException nsfe)
436 errorMessage = nsfe.getMessage();
439 } catch (NoSuchMethodException nsme)
441 errorMessage = nsme.getMessage();
444 } catch (SecurityException se)
446 errorMessage = se.getMessage();
449 } catch (IllegalAccessException iae)
451 errorMessage = iae.getMessage();
462 Class linker = Class.forName("com.apple.mrj.jdirect.Linker");
463 Constructor constructor = linker
464 .getConstructor(new Class[]
466 linkage = constructor
467 .newInstance(new Object[]
468 { BrowserLauncher.class });
469 } catch (ClassNotFoundException cnfe)
471 errorMessage = cnfe.getMessage();
474 } catch (NoSuchMethodException nsme)
476 errorMessage = nsme.getMessage();
479 } catch (InvocationTargetException ite)
481 errorMessage = ite.getMessage();
484 } catch (InstantiationException ie)
486 errorMessage = ie.getMessage();
489 } catch (IllegalAccessException iae)
491 errorMessage = iae.getMessage();
502 mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils");
503 openURL = mrjFileUtilsClass.getDeclaredMethod("openURL",
506 } catch (ClassNotFoundException cnfe)
508 errorMessage = cnfe.getMessage();
511 } catch (NoSuchMethodException nsme)
513 errorMessage = nsme.getMessage();
529 * Attempts to locate the default web browser on the local system. s results
530 * so it only locates the browser once for each use of this class per JVM
533 * @return The browser for the system. Note that this may not be what you
534 * would consider to be a standard web browser; instead, it's the
535 * application that gets called to open the default web browser. In
536 * some cases, this will be a non-String object that provides the
537 * means of calling the default browser.
539 private static Object locateBrowser()
557 Integer finderCreatorCode = (Integer) makeOSType.invoke(null,
560 Object aeTarget = aeTargetConstructor
561 .newInstance(new Object[]
562 { finderCreatorCode });
563 Integer gurlType = (Integer) makeOSType.invoke(null,
566 Object appleEvent = appleEventConstructor
567 .newInstance(new Object[]
568 { gurlType, gurlType, aeTarget, kAutoGenerateReturnID,
569 kAnyTransactionID });
571 // Don't set browser = appleEvent because then the next time we call
572 // locateBrowser(), we'll get the same AppleEvent, to which we'll
574 // added the relevant parameter. Instead, regenerate the AppleEvent
576 // There's probably a way to do this better; if any has any ideas,
580 } catch (IllegalAccessException iae)
583 errorMessage = iae.getMessage();
586 } catch (InstantiationException ie)
589 errorMessage = ie.getMessage();
592 } catch (InvocationTargetException ite)
595 errorMessage = ite.getMessage();
606 systemFolder = (File) findFolder.invoke(null,
608 { kSystemFolderType });
609 } catch (IllegalArgumentException iare)
612 errorMessage = iare.getMessage();
615 } catch (IllegalAccessException iae)
618 errorMessage = iae.getMessage();
621 } catch (InvocationTargetException ite)
624 errorMessage = ite.getTargetException().getClass() + ": "
625 + ite.getTargetException().getMessage();
630 String[] systemFolderFiles = systemFolder.list();
632 // Avoid a FilenameFilter because that can't be stopped mid-list
633 for (int i = 0; i < systemFolderFiles.length; i++)
637 File file = new File(systemFolder, systemFolderFiles[i]);
644 // We're looking for a file with a creator code of 'MACS' and
645 // a type of 'FNDR'. Only requiring the type results in non-Finder
646 // applications being picked up on certain Mac OS 9 systems,
647 // especially German ones, and sending a GURL event to those
648 // applications results in a logout under Multiple Users.
649 Object fileType = getFileType.invoke(null, new Object[] { file });
651 if (FINDER_TYPE.equals(fileType.toString()))
653 Object fileCreator = getFileCreator.invoke(null,
657 if (FINDER_CREATOR.equals(fileCreator.toString()))
659 browser = file.toString(); // Actually the Finder, but that's OK
664 } catch (IllegalArgumentException iare)
666 errorMessage = iare.getMessage();
669 } catch (IllegalAccessException iae)
672 errorMessage = iae.getMessage();
675 } catch (InvocationTargetException ite)
678 errorMessage = ite.getTargetException().getClass() + ": "
679 + ite.getTargetException().getMessage();
691 browser = ""; // Return something non-null
701 browser = "command.com";
707 browser = jalview.bin.Cache.getDefault("DEFAULT_BROWSER", "firefox");
719 * used to ensure that browser is up-to-date after a configuration change
720 * (Unix DEFAULT_BROWSER property change).
722 public static void resetBrowser()
728 * Attempts to open the default web browser to the given URL.
732 * @throws IOException
733 * If the web browser could not be located or does not run
735 public static void openURL(String url) throws IOException
738 if (Platform.openURL(url))
749 if (!loadedWithoutErrors)
751 throw new IOException(MessageManager
752 .formatMessage("exception.browser_not_found", new String[]
756 Object browser = locateBrowser();
760 throw new IOException(MessageManager.formatMessage(
761 "exception.browser_unable_to_locate", new String[]
769 Object aeDesc = null;
773 aeDesc = aeDescConstructor.newInstance(new Object[] { url });
774 putParameter.invoke(browser,
776 { keyDirectObject, aeDesc });
777 sendNoReply.invoke(browser, new Object[] {});
778 } catch (InvocationTargetException ite)
780 throw new IOException(MessageManager.formatMessage(
781 "exception.invocation_target_exception_creating_aedesc",
783 { ite.getMessage() }));
784 } catch (IllegalAccessException iae)
786 throw new IOException(MessageManager.formatMessage(
787 "exception.illegal_access_building_apple_evt", new String[]
788 { iae.getMessage() }));
789 } catch (InstantiationException ie)
791 throw new IOException(MessageManager.formatMessage(
792 "exception.illegal_access_building_apple_evt", new String[]
793 { ie.getMessage() }));
796 aeDesc = null; // Encourage it to get disposed if it was created
797 browser = null; // Ditto
803 Runtime.getRuntime().exec(new String[] { (String) browser, url });
809 int[] instance = new int[1];
810 int result = ICStart(instance, 0);
814 int[] selectionStart = new int[] { 0 };
815 byte[] urlBytes = url.getBytes();
816 int[] selectionEnd = new int[] { urlBytes.length };
817 result = ICLaunchURL(instance[0], new byte[] { 0 }, urlBytes,
818 urlBytes.length, selectionStart, selectionEnd);
822 // Ignore the return value; the URL was launched successfully
823 // regardless of what happens here.
828 throw new IOException(MessageManager.formatMessage(
829 "exception.unable_to_launch_url", new String[]
830 { Integer.valueOf(result).toString() }));
835 throw new IOException(MessageManager.formatMessage(
836 "exception.unable_to_create_internet_config", new String[]
837 { Integer.valueOf(result).toString() }));
846 openURL.invoke(null, new Object[] { url });
847 } catch (InvocationTargetException ite)
849 throw new IOException(MessageManager.formatMessage(
850 "exception.invocation_target_calling_url", new String[]
851 { ite.getMessage() }));
852 } catch (IllegalAccessException iae)
854 throw new IOException(MessageManager.formatMessage(
855 "exception.illegal_access_calling_url", new String[]
856 { iae.getMessage() }));
864 // Add quotes around the URL to allow ampersands and other special
865 // characters to work.
866 Process process = Runtime.getRuntime()
868 { (String) browser, FIRST_WINDOWS_PARAMETER,
869 SECOND_WINDOWS_PARAMETER, THIRD_WINDOWS_PARAMETER,
872 // This avoids a memory leak on some versions of Java on Windows.
873 // That's hinted at in
874 // <http://developer.java.sun.com/developer/qow/archive/68/>.
879 } catch (InterruptedException ie)
881 throw new IOException(MessageManager.formatMessage(
882 "exception.interrupted_launching_browser", new String[]
883 { ie.getMessage() }));
890 // Assume that we're on Unix and that Netscape (actually Firefox) is
892 // First, attempt to open the URL in a currently running session of
897 * System.out.println("Executing : "+browser+" "+
898 * NETSCAPE_REMOTE_PARAMETER+" "+ NETSCAPE_OPEN_PARAMETER_START + url +
899 * NETSCAPE_OPEN_NEW_WINDOW + NETSCAPE_OPEN_PARAMETER_END);
901 process = Runtime.getRuntime()
903 { (String) browser, NETSCAPE_REMOTE_PARAMETER,
905 NETSCAPE_OPEN_PARAMETER_START + url
906 + NETSCAPE_OPEN_NEW_WINDOW
907 + NETSCAPE_OPEN_PARAMETER_END });
911 int exitCode = process.waitFor();
914 { // if Netscape was not open
915 Runtime.getRuntime().exec(new String[] { (String) browser, url });
917 } catch (InterruptedException ie)
919 throw new IOException(MessageManager.formatMessage(
920 "exception.interrupted_launching_browser", new String[]
921 { ie.getMessage() }));
928 // This should never occur, but if it does, we'll try the simplest thing
930 Runtime.getRuntime().exec(new String[] { (String) browser, url });
939 * Methods required for Mac OS X. The presence of native methods does not
940 * cause any problems on other platforms.
942 private native static int ICStart(int[] instance, int signature);
944 private native static int ICStop(int[] instance);
946 private native static int ICLaunchURL(int instance, byte[] hint,
947 byte[] data, int len, int[] selectionStart, int[] selectionEnd);