Source code: com/gui/JspmBrowserLauncher.java
1 package com.gui;
2
3 import java.io.*;
4 import java.lang.reflect.*;
5
6 /**
7 * BrowserLauncher is a class that provides one static method, openURL, which opens the default
8 * web browser for the current user of the system to the given URL. It may support other
9 * protocols depending on the system -- mailto, ftp, etc. -- but that has not been rigorously
10 * tested and is not guaranteed to work.
11 * <p>
12 * Yes, this is platform-specific code, and yes, it may rely on classes on certain platforms
13 * that are not part of the standard JDK. What we're trying to do, though, is to take something
14 * that's frequently desirable but inherently platform-specific -- opening a default browser --
15 * and allow programmers (you, for example) to do so without worrying about dropping into native
16 * code or doing anything else similarly evil.
17 * <p>
18 * Anyway, this code is completely in Java and will run on all JDK 1.1-compliant systems without
19 * modification or a need for additional libraries. All classes that are required on certain
20 * platforms to allow this to run are dynamically loaded at runtime via reflection and, if not
21 * found, will not cause this to do anything other than returning an error when opening the
22 * browser.
23 * <p>
24 * There are certain system requirements for this class, as it's running through Runtime.exec(),
25 * which is Java's way of making a native system call. Currently, this requires that a Macintosh
26 * have a Finder which supports the GURL event, which is true for Mac OS 8.0 and 8.1 systems that
27 * have the Internet Scripting AppleScript dictionary installed in the Scripting Additions folder
28 * in the Extensions folder (which is installed by default as far as I know under Mac OS 8.0 and
29 * 8.1), and for all Mac OS 8.5 and later systems. On Windows, it only runs under Win32 systems
30 * (Windows 95, 98, and NT 4.0, as well as later versions of all). On other systems, this drops
31 * back from the inherently platform-sensitive concept of a default browser and simply attempts
32 * to launch Netscape via a shell command.
33 * <p>
34 * This code is Copyright 1999 by Eric Albert (ejalbert@cs.stanford.edu) and may be redistributed
35 * or modified in any form without restrictions as long as the portion of this comment from this
36 * paragraph through the end of the comment is not removed. The author requests that he be
37 * notified of any application, applet, or other binary that makes use of this code, but that's
38 * more out of curiosity than anything and is not required. This software includes no warranty.
39 * <p>
40 * Credits:
41 * <br>Steven Spencer, JavaWorld magazine (<a href="http://www.javaworld.com/javaworld/javatips/jw-javatip66.html">Java Tip 66</a>)
42 * <br>Ron B. Yeh, ZeroG
43 * <br>Ben Engber, The New York Times
44 * <br>Paul Teitlebaum and Andrea Cantatore, Datatech Software
45 *
46 * @author Eric Albert (<a href="mailto:ejalbert@cs.stanford.edu">ejalbert@cs.stanford.edu</a>)
47 * @version 1.2 (Released July 28, 1999)
48 */
49 public class JspmBrowserLauncher {
50
51 /**
52 * The Java virtual machine that we are running on. Actually, in most cases we only care
53 * about the operating system, but some operating systems require us to switch on the VM. */
54 private static int jvm;
55
56 /** The browser for the system */
57 private static Object browser;
58
59 /**
60 * Caches whether any classes, methods, and fields that are not part of the JDK and need to
61 * be dynamically loaded at runtime loaded successfully.
62 * <p>
63 * Note that if this is <code>false</code>, <code>openURL()</code> will always return an
64 * IOException.
65 */
66 private static boolean loadedWithoutErrors;
67
68 /** The com.apple.mrj.MRJFileUtils class */
69 private static Class mrjFileUtilsClass;
70
71 /** The com.apple.mrj.MRJOSType class */
72 private static Class mrjOSTypeClass;
73
74 /** The com.apple.MacOS.MacOSError class */
75 private static Class macOSErrorClass;
76
77 /** The com.apple.MacOS.AEDesc class */
78 private static Class aeDescClass;
79
80 /** The <init>(int) method of com.apple.MacOS.AETarget */
81 private static Constructor aeTargetConstructor;
82
83 /** The <init>(int, int, int) method of com.apple.MacOS.AppleEvent */
84 private static Constructor appleEventConstructor;
85
86 /** The <init>(String) method of com.apple.MacOS.AEDesc */
87 private static Constructor aeDescConstructor;
88
89 /** The findFolder method of com.apple.mrj.MRJFileUtils */
90 private static Method findFolder;
91
92 /** The getFileType method of com.apple.mrj.MRJOSType */
93 private static Method getFileType;
94
95 /** The makeOSType method of com.apple.MacOS.OSUtils */
96 private static Method makeOSType;
97
98 /** The putParameter method of com.apple.MacOS.AppleEvent */
99 private static Method putParameter;
100
101 /** The sendNoReply method of com.apple.MacOS.AppleEvent */
102 private static Method sendNoReply;
103
104 /** Actually an MRJOSType pointing to the System Folder on a Macintosh */
105 private static Object kSystemFolderType;
106
107 /** The keyDirectObject AppleEvent parameter type */
108 private static Integer keyDirectObject;
109
110 /** The kAutoGenerateReturnID AppleEvent code */
111 private static Integer kAutoGenerateReturnID;
112
113 /** The kAnyTransactionID AppleEvent code */
114 private static Integer kAnyTransactionID;
115
116 /** JVM constant for MRJ 2.0 */
117 private static final int MRJ_2_0 = 0;
118
119 /** JVM constant for MRJ 2.1 or later */
120 private static final int MRJ_2_1 = 1;
121
122 /** JVM constant for any Windows NT JVM */
123 private static final int WINDOWS_NT = 2;
124
125 /** JVM constant for any Windows 9x JVM */
126 private static final int WINDOWS_9x = 3;
127
128 /** JVM constant for any other platform */
129 private static final int OTHER = -1;
130
131 /**
132 * The file type of the Finder on a Macintosh. Hardcoding "Finder" would keep non-U.S. English
133 * systems from working properly.
134 */
135 private static final String FINDER_TYPE = "FNDR";
136
137 /**
138 * The creator code of the Finder on a Macintosh, which is needed to send AppleEvents to the
139 * application.
140 */
141 private static final String FINDER_CREATOR = "MACS";
142
143 /** The name for the AppleEvent type corresponding to a GetURL event. */
144 private static final String GURL_EVENT = "GURL";
145
146 /**
147 * The first parameter that needs to be passed into Runtime.exec() to open the default web
148 * browser on Windows.
149 */
150 private static final String FIRST_WINDOWS_PARAMETER = "/c";
151
152 /** The second parameter for Runtime.exec() on Windows. */
153 private static final String SECOND_WINDOWS_PARAMETER = "start";
154
155 /**
156 * The shell parameters for Netscape that opens a given URL in an already-open copy of Netscape
157 * on many command-line systems.
158 */
159 private static final String NETSCAPE_OPEN_PARAMETER_START = " -remote 'openURL(";
160 private static final String NETSCAPE_OPEN_PARAMETER_END = ")'";
161
162 /**
163 * The message from any exception thrown throughout the initialization process.
164 */
165 private static String errorMessage;
166
167 /**
168 * An initialization block that determines the operating system and loads the necessary
169 * runtime data.
170 */
171 static {
172 loadedWithoutErrors = true;
173 String osName = System.getProperty("os.name");
174 if ("Mac OS".equals(osName)) {
175 String mrjVersion = System.getProperty("mrj.version");
176 String majorMRJVersion = mrjVersion.substring(0, 3);
177 try {
178 double version = Double.valueOf(majorMRJVersion).doubleValue();
179 if (version == 2) {
180 jvm = MRJ_2_0;
181 } else if (version >= 2.1) {
182 // For the time being, assume that all post-2.0 versions of MRJ work the same
183 jvm = MRJ_2_1;
184 } else {
185 loadedWithoutErrors = false;
186 errorMessage = "Unsupported MRJ version: " + version;
187 }
188 } catch (NumberFormatException nfe) {
189 loadedWithoutErrors = false;
190 errorMessage = "Invalid MRJ version: " + mrjVersion;
191 }
192 } else if (osName.startsWith("Windows")) {
193 if (osName.indexOf("9") != -1) {
194 jvm = WINDOWS_9x;
195 } else {
196 jvm = WINDOWS_NT;
197 }
198 } else {
199 jvm = OTHER;
200 }
201
202 if (loadedWithoutErrors) { // if we haven't hit any errors yet
203 loadedWithoutErrors = loadClasses();
204 }
205 }
206
207 /**
208 * This class should be never be instantiated; this just ensures so.
209 */
210 private JspmBrowserLauncher() { }
211
212 /**
213 * Called by a static initializer to load any classes, fields, and methods required at runtime
214 * to locate the user's web browser.
215 * @return <code>true</code> if all intialization succeeded
216 * <code>false</code> if any portion of the initialization failed
217 */
218 private static boolean loadClasses() {
219 switch (jvm) {
220 case MRJ_2_0:
221 try {
222 Class aeTargetClass = Class.forName("com.apple.MacOS.AETarget");
223 macOSErrorClass = Class.forName("com.apple.MacOS.MacOSError");
224 Class osUtilsClass = Class.forName("com.apple.MacOS.OSUtils");
225 Class appleEventClass = Class.forName("com.apple.MacOS.AppleEvent");
226 Class aeClass = Class.forName("com.apple.MacOS.ae");
227 aeDescClass = Class.forName("com.apple.MacOS.AEDesc");
228
229 aeTargetConstructor = aeTargetClass.getDeclaredConstructor(new Class [] { int.class });
230 appleEventConstructor = appleEventClass.getDeclaredConstructor(new Class[] { int.class, int.class, aeTargetClass, int.class, int.class });
231 aeDescConstructor = aeDescClass.getDeclaredConstructor(new Class[] { String.class });
232
233 makeOSType = osUtilsClass.getDeclaredMethod("makeOSType", new Class [] { String.class });
234 putParameter = appleEventClass.getDeclaredMethod("putParameter", new Class[] { int.class, aeDescClass });
235 sendNoReply = appleEventClass.getDeclaredMethod("sendNoReply", new Class[] { });
236
237 Field keyDirectObjectField = aeClass.getDeclaredField("keyDirectObject");
238 keyDirectObject = (Integer) keyDirectObjectField.get(null);
239 Field autoGenerateReturnIDField = appleEventClass.getDeclaredField("kAutoGenerateReturnID");
240 kAutoGenerateReturnID = (Integer) autoGenerateReturnIDField.get(null);
241 Field anyTransactionIDField = appleEventClass.getDeclaredField("kAnyTransactionID");
242 kAnyTransactionID = (Integer) anyTransactionIDField.get(null);
243 } catch (ClassNotFoundException cnfe) {
244 errorMessage = cnfe.getMessage();
245 return false;
246 } catch (NoSuchMethodException nsme) {
247 errorMessage = nsme.getMessage();
248 return false;
249 } catch (NoSuchFieldException nsfe) {
250 errorMessage = nsfe.getMessage();
251 return false;
252 } catch (IllegalAccessException iae) {
253 errorMessage = iae.getMessage();
254 return false;
255 }
256 break;
257 case MRJ_2_1:
258 try {
259 mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils");
260 mrjOSTypeClass = Class.forName("com.apple.mrj.MRJOSType");
261 Field systemFolderField = mrjFileUtilsClass.getDeclaredField("kSystemFolderType");
262 kSystemFolderType = systemFolderField.get(null);
263 findFolder = mrjFileUtilsClass.getDeclaredMethod("findFolder", new Class[] { mrjOSTypeClass });
264 getFileType = mrjFileUtilsClass.getDeclaredMethod("getFileType", new Class[] { File.class });
265 } catch (ClassNotFoundException cnfe) {
266 errorMessage = cnfe.getMessage();
267 return false;
268 } catch (NoSuchFieldException nsfe) {
269 errorMessage = nsfe.getMessage();
270 return false;
271 } catch (NoSuchMethodException nsme) {
272 errorMessage = nsme.getMessage();
273 return false;
274 } catch (SecurityException se) {
275 errorMessage = se.getMessage();
276 return false;
277 } catch (IllegalAccessException iae) {
278 errorMessage = iae.getMessage();
279 return false;
280 }
281 break;
282 }
283 return true;
284 }
285
286 /**
287 * Attempts to locate the default web browser on the local system. Caches results so it
288 * only locates the browser once for each use of this class per JVM instance.
289 * @return The browser for the system. Note that this may not be what you would consider
290 * to be a standard web browser; instead, it's the application that gets called to
291 * open the default web browser. In some cases, this will be a non-String object
292 * that provides the means of calling the default browser.
293 */
294 private static Object locateBrowser() {
295 if (browser != null) {
296 return browser;
297 }
298 switch (jvm) {
299 case MRJ_2_0:
300 try {
301 Integer finderCreatorCode = (Integer) makeOSType.invoke(null, new Object[] { FINDER_CREATOR });
302 Object aeTarget = aeTargetConstructor.newInstance(new Object[] { finderCreatorCode });
303 Integer gurlType = (Integer) makeOSType.invoke(null, new Object[] { GURL_EVENT });
304 Object appleEvent = appleEventConstructor.newInstance(new Object[] { gurlType, gurlType, aeTarget, kAutoGenerateReturnID, kAnyTransactionID });
305 // Don't set browser = appleEvent because then the next time we call
306 // locateBrowser(), we'll get the same AppleEvent, to which we'll already have
307 // added the relevant parameter. Instead, regenerate the AppleEvent every time.
308 // There's probably a way to do this better; if any has any ideas, please let
309 // me know.
310 return appleEvent;
311 } catch (IllegalAccessException iae) {
312 browser = null;
313 errorMessage = iae.getMessage();
314 return browser;
315 } catch (InstantiationException ie) {
316 browser = null;
317 errorMessage = ie.getMessage();
318 return browser;
319 } catch (InvocationTargetException ite) {
320 browser = null;
321 errorMessage = ite.getMessage();
322 return browser;
323 }
324 case MRJ_2_1:
325 File systemFolder;
326 try {
327 systemFolder = (File) findFolder.invoke(null, new Object[] { kSystemFolderType });
328 } catch (IllegalArgumentException iare) {
329 browser = null;
330 errorMessage = iare.getMessage();
331 return browser;
332 } catch (IllegalAccessException iae) {
333 browser = null;
334 errorMessage = iae.getMessage();
335 return browser;
336 } catch (InvocationTargetException ite) {
337 browser = null;
338 errorMessage = ite.getTargetException().getClass() + ": " + ite.getTargetException().getMessage();
339 return browser;
340 }
341 String[] systemFolderFiles = systemFolder.list();
342 // Avoid a FilenameFilter because that can't be stopped mid-list
343 for(int i = 0; i < systemFolderFiles.length; i++) {
344 try {
345 File file = new File(systemFolder, systemFolderFiles[i]);
346 if (!file.isFile()) {
347 continue;
348 }
349 Object fileType = getFileType.invoke(null, new Object[] { file });
350 if (FINDER_TYPE.equals(fileType.toString())) {
351 browser = file.toString(); // Actually the Finder, but that's OK
352 return browser;
353 }
354 } catch (IllegalArgumentException iare) {
355 browser = browser;
356 errorMessage = iare.getMessage();
357 return null;
358 } catch (IllegalAccessException iae) {
359 browser = null;
360 errorMessage = iae.getMessage();
361 return browser;
362 } catch (InvocationTargetException ite) {
363 browser = null;
364 errorMessage = ite.getTargetException().getClass() + ": " + ite.getTargetException().getMessage();
365 return browser;
366 }
367 }
368 browser = null;
369 break;
370 case WINDOWS_NT:
371 browser = "cmd.exe";
372 break;
373 case WINDOWS_9x:
374 browser = "command.com";
375 break;
376 case OTHER:
377 default:
378 browser = "netscape";
379 break;
380 }
381 return browser;
382 }
383
384 /**
385 * Attempts to open the default web browser to the given URL.
386 * @param url The URL to open
387 * @throws IOException If the web browser could not be located or does not run
388 */
389 public static void openURL(String url) throws IOException {
390 if (!loadedWithoutErrors) {
391 throw new IOException("Exception in finding browser: " + errorMessage);
392 }
393 Object browser = locateBrowser();
394 if (browser == null) {
395 throw new IOException("Unable to locate browser: " + errorMessage);
396 }
397 switch (jvm) {
398 case MRJ_2_0:
399 Object aeDesc = null;
400 try {
401 aeDesc = aeDescConstructor.newInstance(new Object[] { url });
402 putParameter.invoke(browser, new Object[] { keyDirectObject, aeDesc });
403 sendNoReply.invoke(browser, new Object[] { });
404 } catch (InvocationTargetException ite) {
405 throw new IOException("InvocationTargetException while creating AEDesc: " + ite.getMessage());
406 } catch (IllegalAccessException iae) {
407 throw new IOException("IllegalAccessException while building AppleEvent: " + iae.getMessage());
408 } catch (InstantiationException ie) {
409 throw new IOException("InstantiationException while creating AEDesc: " + ie.getMessage());
410 } finally {
411 aeDesc = null; // Encourage it to get disposed if it was created
412 browser = null; // Ditto
413 }
414 break;
415 case MRJ_2_1:
416 Runtime.getRuntime().exec(new String[] { (String) browser, url } );
417 break;
418 case WINDOWS_NT:
419 case WINDOWS_9x:
420 Runtime.getRuntime().exec(new String[] { (String) browser, FIRST_WINDOWS_PARAMETER,
421 SECOND_WINDOWS_PARAMETER, url });
422 break;
423 case OTHER:
424 // Assume that we're on Unix and that Netscape is installed
425
426 // First, attempt to open the URL in a currently running session of Netscape
427 Process process = Runtime.getRuntime().exec((String) browser +
428 NETSCAPE_OPEN_PARAMETER_START + url +
429 NETSCAPE_OPEN_PARAMETER_END);
430 try {
431 int exitCode = process.waitFor();
432 if (exitCode != 0) { // if Netscape was not open
433 Runtime.getRuntime().exec(new String[] { (String) browser, url });
434 }
435 } catch (InterruptedException ie) {
436 throw new IOException("InterruptedException while launching browser: " + ie.getMessage());
437 }
438 break;
439 default:
440 // This should never occur, but if it does, we'll try the simplest thing possible
441 Runtime.getRuntime().exec(new String[] { (String) browser, url });
442 break;
443 }
444 }
445 }
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468