001: package gruntspud.ui;
002:
003: import java.io.File;
004: import java.io.IOException;
005: import java.lang.reflect.Constructor;
006: import java.lang.reflect.Field;
007: import java.lang.reflect.InvocationTargetException;
008: import java.lang.reflect.Method;
009:
010: /**
011: * BrowserLauncher is a class that provides one static method, openURL, which opens the default
012: * web browser for the current user of the system to the given URL. It may support other
013: * protocols depending on the system -- mailto, ftp, etc. -- but that has not been rigorously
014: * tested and is not guaranteed to work.
015: * <p>
016: * Yes, this is platform-specific code, and yes, it may rely on classes on certain platforms
017: * that are not part of the standard JDK. What we're trying to do, though, is to take something
018: * that's frequently desirable but inherently platform-specific -- opening a default browser --
019: * and allow programmers (you, for example) to do so without worrying about dropping into native
020: * code or doing anything else similarly evil.
021: * <p>
022: * Anyway, this code is completely in Java and will run on all JDK 1.1-compliant systems without
023: * modification or a need for additional libraries. All classes that are required on certain
024: * platforms to allow this to run are dynamically loaded at runtime via reflection and, if not
025: * found, will not cause this to do anything other than returning an error when opening the
026: * browser.
027: * <p>
028: * There are certain system requirements for this class, as it's running through Runtime.exec(),
029: * which is Java's way of making a native system call. Currently, this requires that a Macintosh
030: * have a Finder which supports the GURL event, which is true for Mac OS 8.0 and 8.1 systems that
031: * have the Internet Scripting AppleScript dictionary installed in the Scripting Additions folder
032: * in the Extensions folder (which is installed by default as far as I know under Mac OS 8.0 and
033: * 8.1), and for all Mac OS 8.5 and later systems. On Windows, it only runs under Win32 systems
034: * (Windows 95, 98, and NT 4.0, as well as later versions of all). On other systems, this drops
035: * back from the inherently platform-sensitive concept of a default browser and simply attempts
036: * to launch Netscape via a shell command.
037: * <p>
038: * This code is Copyright 1999 by Eric Albert (ejalbert@cs.stanford.edu) and may be redistributed
039: * or modified in any form without restrictions as long as the portion of this comment from this
040: * paragraph through the end of the comment is not removed. The author requests that he be
041: * notified of any application, applet, or other binary that makes use of this code, but that's
042: * more out of curiosity than anything and is not required. This software includes no warranty.
043: * <p>
044: * Credits:
045: * <br>Steven Spencer, JavaWorld magazine (<a href="http://www.javaworld.com/javaworld/javatips/jw-javatip66.html">Java Tip 66</a>)
046: * <br>Ron B. Yeh, ZeroG
047: * <br>Ben Engber, The New York Times
048: * <br>Paul Teitlebaum and Andrea Cantatore, Datatech Software
049: *
050: * @author Eric Albert (<a href="mailto:ejalbert@cs.stanford.edu">ejalbert@cs.stanford.edu</a>)
051: * @version 1.2 (Released July 28, 1999)
052: */
053: public class BrowserLauncher {
054: /**
055: * The Java virtual machine that we are running on. Actually, in most cases we only care
056: * about the operating system, but some operating systems require us to switch on the VM. */
057: private static int jvm;
058:
059: /** The browser for the system */
060: private static Object browser;
061:
062: /**
063: * Caches whether any classes, methods, and fields that are not part of the JDK and need to
064: * be dynamically loaded at runtime loaded successfully.
065: * <p>
066: * Note that if this is <code>false</code>, <code>openURL()</code> will always return an
067: * IOException.
068: */
069: private static boolean loadedWithoutErrors;
070:
071: /** The com.apple.mrj.MRJFileUtils class */
072: private static Class mrjFileUtilsClass;
073:
074: /** The com.apple.mrj.MRJOSType class */
075: private static Class mrjOSTypeClass;
076:
077: /** The com.apple.MacOS.MacOSError class */
078: private static Class macOSErrorClass;
079:
080: /** The com.apple.MacOS.AEDesc class */
081: private static Class aeDescClass;
082:
083: /** The <init>(int) method of com.apple.MacOS.AETarget */
084: private static Constructor aeTargetConstructor;
085:
086: /** The <init>(int, int, int) method of com.apple.MacOS.AppleEvent */
087: private static Constructor appleEventConstructor;
088:
089: /** The <init>(String) method of com.apple.MacOS.AEDesc */
090: private static Constructor aeDescConstructor;
091:
092: /** The findFolder method of com.apple.mrj.MRJFileUtils */
093: private static Method findFolder;
094:
095: /** The getFileType method of com.apple.mrj.MRJOSType */
096: private static Method getFileType;
097:
098: /** The makeOSType method of com.apple.MacOS.OSUtils */
099: private static Method makeOSType;
100:
101: /** The putParameter method of com.apple.MacOS.AppleEvent */
102: private static Method putParameter;
103:
104: /** The sendNoReply method of com.apple.MacOS.AppleEvent */
105: private static Method sendNoReply;
106:
107: /** Actually an MRJOSType pointing to the System Folder on a Macintosh */
108: private static Object kSystemFolderType;
109:
110: /** The keyDirectObject AppleEvent parameter type */
111: private static Integer keyDirectObject;
112:
113: /** The kAutoGenerateReturnID AppleEvent code */
114: private static Integer kAutoGenerateReturnID;
115:
116: /** The kAnyTransactionID AppleEvent code */
117: private static Integer kAnyTransactionID;
118:
119: /** JVM constant for MRJ 2.0 */
120: private static final int MRJ_2_0 = 0;
121:
122: /** JVM constant for MRJ 2.1 or later */
123: private static final int MRJ_2_1 = 1;
124:
125: /** JVM constant for any Windows NT JVM */
126: private static final int WINDOWS_NT = 2;
127:
128: /** JVM constant for any Windows 9x JVM */
129: private static final int WINDOWS_9x = 3;
130:
131: /** JVM constant for any other platform */
132: private static final int OTHER = -1;
133:
134: /**
135: * The file type of the Finder on a Macintosh. Hardcoding "Finder" would keep non-U.S. English
136: * systems from working properly.
137: */
138: private static final String FINDER_TYPE = "FNDR";
139:
140: /**
141: * The creator code of the Finder on a Macintosh, which is needed to send AppleEvents to the
142: * application.
143: */
144: private static final String FINDER_CREATOR = "MACS";
145:
146: /** The name for the AppleEvent type corresponding to a GetURL event. */
147: private static final String GURL_EVENT = "GURL";
148:
149: /**
150: * The first parameter that needs to be passed into Runtime.exec() to open the default web
151: * browser on Windows.
152: */
153: private static final String FIRST_WINDOWS_PARAMETER = "/c";
154:
155: /** The second parameter for Runtime.exec() on Windows. */
156: private static final String SECOND_WINDOWS_PARAMETER = "start";
157:
158: /**
159: * The shell parameters for Netscape that opens a given URL in an already-open copy of Netscape
160: * on many command-line systems.
161: */
162: private static final String NETSCAPE_OPEN_PARAMETER_START = " -remote 'openURL(";
163: private static final String NETSCAPE_OPEN_PARAMETER_END = ")'";
164:
165: /**
166: * The message from any exception thrown throughout the initialization process.
167: */
168: private static String errorMessage;
169:
170: /**
171: * An initialization block that determines the operating system and loads the necessary
172: * runtime data.
173: */
174: static {
175: loadedWithoutErrors = true;
176:
177: String osName = System.getProperty("os.name");
178:
179: if ("Mac OS".equals(osName)) {
180: String mrjVersion = System.getProperty("mrj.version");
181: String majorMRJVersion = mrjVersion.substring(0, 3);
182:
183: try {
184: double version = Double.valueOf(majorMRJVersion)
185: .doubleValue();
186:
187: if (version == 2) {
188: jvm = MRJ_2_0;
189: } else if (version >= 2.1) {
190:
191: // For the time being, assume that all post-2.0 versions of MRJ work the same
192: jvm = MRJ_2_1;
193: } else {
194: loadedWithoutErrors = false;
195: errorMessage = "Unsupported MRJ version: "
196: + version;
197: }
198: } catch (NumberFormatException nfe) {
199: loadedWithoutErrors = false;
200: errorMessage = "Invalid MRJ version: " + mrjVersion;
201: }
202: } else if (osName.startsWith("Windows")) {
203: if (osName.indexOf("9") != -1) {
204: jvm = WINDOWS_9x;
205: } else {
206: jvm = WINDOWS_NT;
207: }
208: } else {
209: jvm = OTHER;
210:
211: }
212: if (loadedWithoutErrors) {
213: loadedWithoutErrors = loadClasses();
214: }
215: }
216:
217: /**
218: * This class should be never be instantiated; this just ensures so.
219: */
220: private BrowserLauncher() {
221: }
222:
223: /**
224: * Called by a static initializer to load any classes, fields, and methods required at runtime
225: * to locate the user's web browser.
226: * @return <code>true</code> if all intialization succeeded
227: * <code>false</code> if any portion of the initialization failed
228: */
229: private static boolean loadClasses() {
230: switch (jvm) {
231: case MRJ_2_0:
232:
233: try {
234: Class aeTargetClass = Class
235: .forName("com.apple.MacOS.AETarget");
236: macOSErrorClass = Class
237: .forName("com.apple.MacOS.MacOSError");
238:
239: Class osUtilsClass = Class
240: .forName("com.apple.MacOS.OSUtils");
241: Class appleEventClass = Class
242: .forName("com.apple.MacOS.AppleEvent");
243: Class aeClass = Class.forName("com.apple.MacOS.ae");
244: aeDescClass = Class.forName("com.apple.MacOS.AEDesc");
245:
246: aeTargetConstructor = aeTargetClass
247: .getDeclaredConstructor(new Class[] { int.class });
248: appleEventConstructor = appleEventClass
249: .getDeclaredConstructor(new Class[] {
250: int.class, int.class, aeTargetClass,
251: int.class, int.class });
252: aeDescConstructor = aeDescClass
253: .getDeclaredConstructor(new Class[] { String.class });
254:
255: makeOSType = osUtilsClass.getDeclaredMethod(
256: "makeOSType", new Class[] { String.class });
257: putParameter = appleEventClass.getDeclaredMethod(
258: "putParameter", new Class[] { int.class,
259: aeDescClass });
260: sendNoReply = appleEventClass.getDeclaredMethod(
261: "sendNoReply", new Class[] {});
262:
263: Field keyDirectObjectField = aeClass
264: .getDeclaredField("keyDirectObject");
265: keyDirectObject = (Integer) keyDirectObjectField
266: .get(null);
267:
268: Field autoGenerateReturnIDField = appleEventClass
269: .getDeclaredField("kAutoGenerateReturnID");
270: kAutoGenerateReturnID = (Integer) autoGenerateReturnIDField
271: .get(null);
272:
273: Field anyTransactionIDField = appleEventClass
274: .getDeclaredField("kAnyTransactionID");
275: kAnyTransactionID = (Integer) anyTransactionIDField
276: .get(null);
277: } catch (ClassNotFoundException cnfe) {
278: errorMessage = cnfe.getMessage();
279:
280: return false;
281: } catch (NoSuchMethodException nsme) {
282: errorMessage = nsme.getMessage();
283:
284: return false;
285: } catch (NoSuchFieldException nsfe) {
286: errorMessage = nsfe.getMessage();
287:
288: return false;
289: } catch (IllegalAccessException iae) {
290: errorMessage = iae.getMessage();
291:
292: return false;
293: }
294:
295: break;
296: case MRJ_2_1:
297:
298: try {
299: mrjFileUtilsClass = Class
300: .forName("com.apple.mrj.MRJFileUtils");
301: mrjOSTypeClass = Class
302: .forName("com.apple.mrj.MRJOSType");
303:
304: Field systemFolderField = mrjFileUtilsClass
305: .getDeclaredField("kSystemFolderType");
306: kSystemFolderType = systemFolderField.get(null);
307: findFolder = mrjFileUtilsClass.getDeclaredMethod(
308: "findFolder", new Class[] { mrjOSTypeClass });
309: getFileType = mrjFileUtilsClass.getDeclaredMethod(
310: "getFileType", new Class[] { File.class });
311: } catch (ClassNotFoundException cnfe) {
312: errorMessage = cnfe.getMessage();
313:
314: return false;
315: } catch (NoSuchFieldException nsfe) {
316: errorMessage = nsfe.getMessage();
317:
318: return false;
319: } catch (NoSuchMethodException nsme) {
320: errorMessage = nsme.getMessage();
321:
322: return false;
323: } catch (SecurityException se) {
324: errorMessage = se.getMessage();
325:
326: return false;
327: } catch (IllegalAccessException iae) {
328: errorMessage = iae.getMessage();
329:
330: return false;
331: }
332:
333: break;
334: }
335:
336: return true;
337: }
338:
339: /**
340: * Attempts to locate the default web browser on the local system. Caches results so it
341: * only locates the browser once for each use of this class per JVM instance.
342: * @return The browser for the system. Note that this may not be what you would consider
343: * to be a standard web browser; instead, it's the application that gets called to
344: * open the default web browser. In some cases, this will be a non-String object
345: * that provides the means of calling the default browser.
346: */
347: private static Object locateBrowser() {
348: if (browser != null) {
349: return browser;
350: }
351:
352: switch (jvm) {
353: case MRJ_2_0:
354:
355: try {
356: Integer finderCreatorCode = (Integer) makeOSType
357: .invoke(null, new Object[] { FINDER_CREATOR });
358: Object aeTarget = aeTargetConstructor
359: .newInstance(new Object[] { finderCreatorCode });
360: Integer gurlType = (Integer) makeOSType.invoke(null,
361: new Object[] { GURL_EVENT });
362: Object appleEvent = appleEventConstructor
363: .newInstance(new Object[] { gurlType, gurlType,
364: aeTarget, kAutoGenerateReturnID,
365: kAnyTransactionID });
366:
367: // Don't set browser = appleEvent because then the next time we call
368: // locateBrowser(), we'll get the same AppleEvent, to which we'll already have
369: // added the relevant parameter. Instead, regenerate the AppleEvent every time.
370: // There's probably a way to do this better; if any has any ideas, please let
371: // me know.
372: return appleEvent;
373: } catch (IllegalAccessException iae) {
374: browser = null;
375: errorMessage = iae.getMessage();
376:
377: return browser;
378: } catch (InstantiationException ie) {
379: browser = null;
380: errorMessage = ie.getMessage();
381:
382: return browser;
383: } catch (InvocationTargetException ite) {
384: browser = null;
385: errorMessage = ite.getMessage();
386:
387: return browser;
388: }
389: case MRJ_2_1:
390:
391: File systemFolder;
392:
393: try {
394: systemFolder = (File) findFolder.invoke(null,
395: new Object[] { kSystemFolderType });
396: } catch (IllegalArgumentException iare) {
397: browser = null;
398: errorMessage = iare.getMessage();
399:
400: return browser;
401: } catch (IllegalAccessException iae) {
402: browser = null;
403: errorMessage = iae.getMessage();
404:
405: return browser;
406: } catch (InvocationTargetException ite) {
407: browser = null;
408: errorMessage = ite.getTargetException().getClass()
409: + ": " + ite.getTargetException().getMessage();
410:
411: return browser;
412: }
413:
414: String[] systemFolderFiles = systemFolder.list();
415:
416: // Avoid a FilenameFilter because that can't be stopped mid-list
417: for (int i = 0; i < systemFolderFiles.length; i++) {
418: try {
419: File file = new File(systemFolder,
420: systemFolderFiles[i]);
421:
422: if (!file.isFile()) {
423: continue;
424: }
425:
426: Object fileType = getFileType.invoke(null,
427: new Object[] { file });
428:
429: if (FINDER_TYPE.equals(fileType.toString())) {
430: browser = file.toString(); // Actually the Finder, but that's OK
431:
432: return browser;
433: }
434: } catch (IllegalArgumentException iare) {
435: browser = browser;
436: errorMessage = iare.getMessage();
437:
438: return null;
439: } catch (IllegalAccessException iae) {
440: browser = null;
441: errorMessage = iae.getMessage();
442:
443: return browser;
444: } catch (InvocationTargetException ite) {
445: browser = null;
446: errorMessage = ite.getTargetException().getClass()
447: + ": "
448: + ite.getTargetException().getMessage();
449:
450: return browser;
451: }
452: }
453:
454: browser = null;
455:
456: break;
457: case WINDOWS_NT:
458: browser = "cmd.exe";
459:
460: break;
461: case WINDOWS_9x:
462: browser = "command.com";
463:
464: break;
465: case OTHER:
466: default:
467: browser = "netscape";
468:
469: break;
470: }
471:
472: return browser;
473: }
474:
475: /**
476: * Attempts to open the default web browser to the given URL.
477: * @param url The URL to open
478: * @throws IOException If the web browser could not be located or does not run
479: */
480: public static void openURL(String url) throws IOException {
481: if (!loadedWithoutErrors) {
482: throw new IOException("Exception in finding browser: "
483: + errorMessage);
484: }
485:
486: Object browser = locateBrowser();
487:
488: if (browser == null) {
489: throw new IOException("Unable to locate browser: "
490: + errorMessage);
491: }
492:
493: switch (jvm) {
494: case MRJ_2_0:
495:
496: Object aeDesc = null;
497:
498: try {
499: aeDesc = aeDescConstructor
500: .newInstance(new Object[] { url });
501: putParameter.invoke(browser, new Object[] {
502: keyDirectObject, aeDesc });
503: sendNoReply.invoke(browser, new Object[] {});
504: } catch (InvocationTargetException ite) {
505: throw new IOException(
506: "InvocationTargetException while creating AEDesc: "
507: + ite.getMessage());
508: } catch (IllegalAccessException iae) {
509: throw new IOException(
510: "IllegalAccessException while building AppleEvent: "
511: + iae.getMessage());
512: } catch (InstantiationException ie) {
513: throw new IOException(
514: "InstantiationException while creating AEDesc: "
515: + ie.getMessage());
516: } finally {
517: aeDesc = null; // Encourage it to get disposed if it was created
518: browser = null; // Ditto
519: }
520:
521: break;
522: case MRJ_2_1:
523: Runtime.getRuntime().exec(
524: new String[] { (String) browser, url });
525:
526: break;
527: case WINDOWS_NT:
528: case WINDOWS_9x:
529: Runtime.getRuntime().exec(
530: new String[] { (String) browser,
531: FIRST_WINDOWS_PARAMETER,
532: SECOND_WINDOWS_PARAMETER, url });
533:
534: break;
535: case OTHER:
536:
537: // Assume that we're on Unix and that Netscape is installed
538: // First, attempt to open the URL in a currently running session of Netscape
539: Process process = Runtime.getRuntime().exec(
540: (String) browser + NETSCAPE_OPEN_PARAMETER_START
541: + url + NETSCAPE_OPEN_PARAMETER_END);
542:
543: try {
544: int exitCode = process.waitFor();
545:
546: if (exitCode != 0) {
547: Runtime.getRuntime().exec(
548: new String[] { (String) browser, url });
549: }
550: } catch (InterruptedException ie) {
551: throw new IOException(
552: "InterruptedException while launching browser: "
553: + ie.getMessage());
554: }
555:
556: break;
557: default:
558:
559: // This should never occur, but if it does, we'll try the simplest thing possible
560: Runtime.getRuntime().exec(
561: new String[] { (String) browser, url });
562:
563: break;
564: }
565: }
566: }
|