001: package net.sf.regain.util.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-2001 by Eric Albert (ejalbert@cs.stanford.edu) and may be
039: * redistributed or modified in any form without restrictions as long as the portion of this
040: * comment from this paragraph through the end of the comment is not removed. The author
041: * requests that he be notified of any application, applet, or other binary that makes use of
042: * this code, but that's more out of curiosity than anything and is not required. This software
043: * includes no warranty. The author is not repsonsible for any loss of data or functionality
044: * or any adverse or unexpected effects of using this software.
045: * <p>
046: * Credits:
047: * <br>Steven Spencer, JavaWorld magazine (<a href="http://www.javaworld.com/javaworld/javatips/jw-javatip66.html">Java Tip 66</a>)
048: * <br>Thanks also to Ron B. Yeh, Eric Shapiro, Ben Engber, Paul Teitlebaum, Andrea Cantatore,
049: * Larry Barowski, Trevor Bedzek, Frank Miedrich, and Ron Rabakukk
050: *
051: * @author Eric Albert (<a href="mailto:ejalbert@cs.stanford.edu">ejalbert@cs.stanford.edu</a>)
052: * @version 1.4b1 (Released June 20, 2001)
053: */
054: public class BrowserLauncher {
055:
056: /**
057: * The Java virtual machine that we are running on. Actually, in most cases we only care
058: * about the operating system, but some operating systems require us to switch on the VM. */
059: private static int jvm;
060:
061: /** The browser for the system */
062: private static Object browser;
063:
064: /**
065: * Caches whether any classes, methods, and fields that are not part of the JDK and need to
066: * be dynamically loaded at runtime loaded successfully.
067: * <p>
068: * Note that if this is <code>false</code>, <code>openURL()</code> will always return an
069: * IOException.
070: */
071: private static boolean loadedWithoutErrors;
072:
073: /** The com.apple.mrj.MRJFileUtils class */
074: private static Class mrjFileUtilsClass;
075:
076: /** The com.apple.mrj.MRJOSType class */
077: private static Class mrjOSTypeClass;
078:
079: /** The com.apple.MacOS.AEDesc class */
080: private static Class aeDescClass;
081:
082: /** The <init>(int) method of com.apple.MacOS.AETarget */
083: private static Constructor aeTargetConstructor;
084:
085: /** The <init>(int, int, int) method of com.apple.MacOS.AppleEvent */
086: private static Constructor appleEventConstructor;
087:
088: /** The <init>(String) method of com.apple.MacOS.AEDesc */
089: private static Constructor aeDescConstructor;
090:
091: /** The findFolder method of com.apple.mrj.MRJFileUtils */
092: private static Method findFolder;
093:
094: /** The getFileCreator method of com.apple.mrj.MRJFileUtils */
095: private static Method getFileCreator;
096:
097: /** The getFileType method of com.apple.mrj.MRJFileUtils */
098: private static Method getFileType;
099:
100: /** The openURL method of com.apple.mrj.MRJFileUtils */
101: private static Method openURL;
102:
103: /** The makeOSType method of com.apple.MacOS.OSUtils */
104: private static Method makeOSType;
105:
106: /** The putParameter method of com.apple.MacOS.AppleEvent */
107: private static Method putParameter;
108:
109: /** The sendNoReply method of com.apple.MacOS.AppleEvent */
110: private static Method sendNoReply;
111:
112: /** Actually an MRJOSType pointing to the System Folder on a Macintosh */
113: private static Object kSystemFolderType;
114:
115: /** The keyDirectObject AppleEvent parameter type */
116: private static Integer keyDirectObject;
117:
118: /** The kAutoGenerateReturnID AppleEvent code */
119: private static Integer kAutoGenerateReturnID;
120:
121: /** The kAnyTransactionID AppleEvent code */
122: private static Integer kAnyTransactionID;
123:
124: /** The linkage object required for JDirect 3 on Mac OS X. */
125: private static Object linkage;
126:
127: /** The framework to reference on Mac OS X */
128: //private static final String JDirect_MacOSX = "/System/Library/Frameworks/Carbon.framework/Frameworks/HIToolbox.framework/HIToolbox";
129: /** JVM constant for MRJ 2.0 */
130: private static final int MRJ_2_0 = 0;
131:
132: /** JVM constant for MRJ 2.1 or later */
133: private static final int MRJ_2_1 = 1;
134:
135: /** JVM constant for Java on Mac OS X 10.0 (MRJ 3.0) */
136: private static final int MRJ_3_0 = 3;
137:
138: /** JVM constant for MRJ 3.1 */
139: private static final int MRJ_3_1 = 4;
140:
141: /** JVM constant for any Windows NT JVM */
142: private static final int WINDOWS_NT = 5;
143:
144: /** JVM constant for any Windows 9x JVM */
145: private static final int WINDOWS_9x = 6;
146:
147: /** JVM constant for any other platform */
148: private static final int OTHER = -1;
149:
150: /**
151: * The file type of the Finder on a Macintosh. Hardcoding "Finder" would keep non-U.S. English
152: * systems from working properly.
153: */
154: private static final String FINDER_TYPE = "FNDR";
155:
156: /**
157: * The creator code of the Finder on a Macintosh, which is needed to send AppleEvents to the
158: * application.
159: */
160: private static final String FINDER_CREATOR = "MACS";
161:
162: /** The name for the AppleEvent type corresponding to a GetURL event. */
163: private static final String GURL_EVENT = "GURL";
164:
165: /**
166: * The first parameter that needs to be passed into Runtime.exec() to open the default web
167: * browser on Windows.
168: */
169: private static final String FIRST_WINDOWS_PARAMETER = "/c";
170:
171: /** The second parameter for Runtime.exec() on Windows. */
172: private static final String SECOND_WINDOWS_PARAMETER = "start";
173:
174: /**
175: * The third parameter for Runtime.exec() on Windows. This is a "title"
176: * parameter that the command line expects. Setting this parameter allows
177: * URLs containing spaces to work.
178: */
179: private static final String THIRD_WINDOWS_PARAMETER = "\"\"";
180:
181: /**
182: * The shell parameters for Netscape that opens a given URL in an already-open copy of Netscape
183: * on many command-line systems.
184: */
185: private static final String NETSCAPE_REMOTE_PARAMETER = "-remote";
186: private static final String NETSCAPE_OPEN_PARAMETER_START = "'openURL(";
187: private static final String NETSCAPE_OPEN_PARAMETER_END = ")'";
188:
189: /**
190: * The message from any exception thrown throughout the initialization process.
191: */
192: private static String errorMessage;
193:
194: /**
195: * An initialization block that determines the operating system and loads the necessary
196: * runtime data.
197: */
198: static {
199: loadedWithoutErrors = true;
200: String osName = System.getProperty("os.name");
201: if (osName.startsWith("Mac OS")) {
202: String mrjVersion = System.getProperty("mrj.version");
203: String majorMRJVersion = mrjVersion.substring(0, 3);
204: try {
205: double version = Double.valueOf(majorMRJVersion)
206: .doubleValue();
207: if (version == 2) {
208: jvm = MRJ_2_0;
209: } else if (version >= 2.1 && version < 3) {
210: // Assume that all 2.x versions of MRJ work the same. MRJ 2.1 actually
211: // works via Runtime.exec() and 2.2 supports that but has an openURL() method
212: // as well that we currently ignore.
213: jvm = MRJ_2_1;
214: } else if (version == 3.0) {
215: jvm = MRJ_3_0;
216: } else if (version >= 3.1) {
217: // Assume that all 3.1 and later versions of MRJ work the same.
218: jvm = MRJ_3_1;
219: } else {
220: loadedWithoutErrors = false;
221: errorMessage = "Unsupported MRJ version: "
222: + version;
223: }
224: } catch (NumberFormatException nfe) {
225: loadedWithoutErrors = false;
226: errorMessage = "Invalid MRJ version: " + mrjVersion;
227: }
228: } else if (osName.startsWith("Windows")) {
229: if (osName.indexOf("9") != -1) {
230: jvm = WINDOWS_9x;
231: } else {
232: jvm = WINDOWS_NT;
233: }
234: } else {
235: jvm = OTHER;
236: }
237:
238: if (loadedWithoutErrors) { // if we haven't hit any errors yet
239: loadedWithoutErrors = loadClasses();
240: }
241: }
242:
243: /**
244: * This class should be never be instantiated; this just ensures so.
245: */
246: private BrowserLauncher() {
247: }
248:
249: public static void setBrowser(String customBrowser) {
250: browser = customBrowser;
251: }
252:
253: /**
254: * Called by a static initializer to load any classes, fields, and methods required at runtime
255: * to locate the user's web browser.
256: * @return <code>true</code> if all intialization succeeded
257: * <code>false</code> if any portion of the initialization failed
258: */
259: private static boolean loadClasses() {
260: switch (jvm) {
261: case MRJ_2_0:
262: try {
263: Class aeTargetClass = Class
264: .forName("com.apple.MacOS.AETarget");
265: Class osUtilsClass = Class
266: .forName("com.apple.MacOS.OSUtils");
267: Class appleEventClass = Class
268: .forName("com.apple.MacOS.AppleEvent");
269: Class aeClass = Class.forName("com.apple.MacOS.ae");
270: aeDescClass = Class.forName("com.apple.MacOS.AEDesc");
271:
272: aeTargetConstructor = aeTargetClass
273: .getDeclaredConstructor(new Class[] { int.class });
274: appleEventConstructor = appleEventClass
275: .getDeclaredConstructor(new Class[] {
276: int.class, int.class, aeTargetClass,
277: int.class, int.class });
278: aeDescConstructor = aeDescClass
279: .getDeclaredConstructor(new Class[] { String.class });
280:
281: makeOSType = osUtilsClass.getDeclaredMethod(
282: "makeOSType", new Class[] { String.class });
283: putParameter = appleEventClass.getDeclaredMethod(
284: "putParameter", new Class[] { int.class,
285: aeDescClass });
286: sendNoReply = appleEventClass.getDeclaredMethod(
287: "sendNoReply", new Class[] {});
288:
289: Field keyDirectObjectField = aeClass
290: .getDeclaredField("keyDirectObject");
291: keyDirectObject = (Integer) keyDirectObjectField
292: .get(null);
293: Field autoGenerateReturnIDField = appleEventClass
294: .getDeclaredField("kAutoGenerateReturnID");
295: kAutoGenerateReturnID = (Integer) autoGenerateReturnIDField
296: .get(null);
297: Field anyTransactionIDField = appleEventClass
298: .getDeclaredField("kAnyTransactionID");
299: kAnyTransactionID = (Integer) anyTransactionIDField
300: .get(null);
301: } catch (ClassNotFoundException cnfe) {
302: errorMessage = cnfe.getMessage();
303: return false;
304: } catch (NoSuchMethodException nsme) {
305: errorMessage = nsme.getMessage();
306: return false;
307: } catch (NoSuchFieldException nsfe) {
308: errorMessage = nsfe.getMessage();
309: return false;
310: } catch (IllegalAccessException iae) {
311: errorMessage = iae.getMessage();
312: return false;
313: }
314: break;
315: case MRJ_2_1:
316: try {
317: mrjFileUtilsClass = Class
318: .forName("com.apple.mrj.MRJFileUtils");
319: mrjOSTypeClass = Class
320: .forName("com.apple.mrj.MRJOSType");
321: Field systemFolderField = mrjFileUtilsClass
322: .getDeclaredField("kSystemFolderType");
323: kSystemFolderType = systemFolderField.get(null);
324: findFolder = mrjFileUtilsClass.getDeclaredMethod(
325: "findFolder", new Class[] { mrjOSTypeClass });
326: getFileCreator = mrjFileUtilsClass.getDeclaredMethod(
327: "getFileCreator", new Class[] { File.class });
328: getFileType = mrjFileUtilsClass.getDeclaredMethod(
329: "getFileType", new Class[] { File.class });
330: } catch (ClassNotFoundException cnfe) {
331: errorMessage = cnfe.getMessage();
332: return false;
333: } catch (NoSuchFieldException nsfe) {
334: errorMessage = nsfe.getMessage();
335: return false;
336: } catch (NoSuchMethodException nsme) {
337: errorMessage = nsme.getMessage();
338: return false;
339: } catch (SecurityException se) {
340: errorMessage = se.getMessage();
341: return false;
342: } catch (IllegalAccessException iae) {
343: errorMessage = iae.getMessage();
344: return false;
345: }
346: break;
347: case MRJ_3_0:
348: try {
349: Class linker = Class
350: .forName("com.apple.mrj.jdirect.Linker");
351: Constructor constructor = linker
352: .getConstructor(new Class[] { Class.class });
353: linkage = constructor
354: .newInstance(new Object[] { BrowserLauncher.class });
355: } catch (ClassNotFoundException cnfe) {
356: errorMessage = cnfe.getMessage();
357: return false;
358: } catch (NoSuchMethodException nsme) {
359: errorMessage = nsme.getMessage();
360: return false;
361: } catch (InvocationTargetException ite) {
362: errorMessage = ite.getMessage();
363: return false;
364: } catch (InstantiationException ie) {
365: errorMessage = ie.getMessage();
366: return false;
367: } catch (IllegalAccessException iae) {
368: errorMessage = iae.getMessage();
369: return false;
370: }
371: break;
372: case MRJ_3_1:
373: try {
374: mrjFileUtilsClass = Class
375: .forName("com.apple.mrj.MRJFileUtils");
376: openURL = mrjFileUtilsClass.getDeclaredMethod(
377: "openURL", new Class[] { String.class });
378: } catch (ClassNotFoundException cnfe) {
379: errorMessage = cnfe.getMessage();
380: return false;
381: } catch (NoSuchMethodException nsme) {
382: errorMessage = nsme.getMessage();
383: return false;
384: }
385: break;
386: default:
387: break;
388: }
389: return true;
390: }
391:
392: /**
393: * Attempts to locate the default web browser on the local system. Caches results so it
394: * only locates the browser once for each use of this class per JVM instance.
395: * @return The browser for the system. Note that this may not be what you would consider
396: * to be a standard web browser; instead, it's the application that gets called to
397: * open the default web browser. In some cases, this will be a non-String object
398: * that provides the means of calling the default browser.
399: */
400: private static Object locateBrowser() {
401: if (browser != null) {
402: return browser;
403: }
404: switch (jvm) {
405: case MRJ_2_0:
406: try {
407: Integer finderCreatorCode = (Integer) makeOSType
408: .invoke(null, new Object[] { FINDER_CREATOR });
409: Object aeTarget = aeTargetConstructor
410: .newInstance(new Object[] { finderCreatorCode });
411: Integer gurlType = (Integer) makeOSType.invoke(null,
412: new Object[] { GURL_EVENT });
413: Object appleEvent = appleEventConstructor
414: .newInstance(new Object[] { gurlType, gurlType,
415: aeTarget, kAutoGenerateReturnID,
416: kAnyTransactionID });
417: // Don't set browser = appleEvent because then the next time we call
418: // locateBrowser(), we'll get the same AppleEvent, to which we'll already have
419: // added the relevant parameter. Instead, regenerate the AppleEvent every time.
420: // There's probably a way to do this better; if any has any ideas, please let
421: // me know.
422: return appleEvent;
423: } catch (IllegalAccessException iae) {
424: browser = null;
425: errorMessage = iae.getMessage();
426: return browser;
427: } catch (InstantiationException ie) {
428: browser = null;
429: errorMessage = ie.getMessage();
430: return browser;
431: } catch (InvocationTargetException ite) {
432: browser = null;
433: errorMessage = ite.getMessage();
434: return browser;
435: }
436: case MRJ_2_1:
437: File systemFolder;
438: try {
439: systemFolder = (File) findFolder.invoke(null,
440: new Object[] { kSystemFolderType });
441: } catch (IllegalArgumentException iare) {
442: browser = null;
443: errorMessage = iare.getMessage();
444: return browser;
445: } catch (IllegalAccessException iae) {
446: browser = null;
447: errorMessage = iae.getMessage();
448: return browser;
449: } catch (InvocationTargetException ite) {
450: browser = null;
451: errorMessage = ite.getTargetException().getClass()
452: + ": " + ite.getTargetException().getMessage();
453: return browser;
454: }
455: String[] systemFolderFiles = systemFolder.list();
456: // Avoid a FilenameFilter because that can't be stopped mid-list
457: for (int i = 0; i < systemFolderFiles.length; i++) {
458: try {
459: File file = new File(systemFolder,
460: systemFolderFiles[i]);
461: if (!file.isFile()) {
462: continue;
463: }
464: // We're looking for a file with a creator code of 'MACS' and
465: // a type of 'FNDR'. Only requiring the type results in non-Finder
466: // applications being picked up on certain Mac OS 9 systems,
467: // especially German ones, and sending a GURL event to those
468: // applications results in a logout under Multiple Users.
469: Object fileType = getFileType.invoke(null,
470: new Object[] { file });
471: if (FINDER_TYPE.equals(fileType.toString())) {
472: Object fileCreator = getFileCreator.invoke(
473: null, new Object[] { file });
474: if (FINDER_CREATOR.equals(fileCreator
475: .toString())) {
476: browser = file.toString(); // Actually the Finder, but that's OK
477: return browser;
478: }
479: }
480: } catch (IllegalArgumentException iare) {
481: browser = browser;
482: errorMessage = iare.getMessage();
483: return null;
484: } catch (IllegalAccessException iae) {
485: browser = null;
486: errorMessage = iae.getMessage();
487: return browser;
488: } catch (InvocationTargetException ite) {
489: browser = null;
490: errorMessage = ite.getTargetException().getClass()
491: + ": "
492: + ite.getTargetException().getMessage();
493: return browser;
494: }
495: }
496: browser = null;
497: break;
498: case MRJ_3_0:
499: case MRJ_3_1:
500: browser = ""; // Return something non-null
501: break;
502: case WINDOWS_NT:
503: browser = "cmd.exe";
504: break;
505: case WINDOWS_9x:
506: browser = "command.com";
507: break;
508: case OTHER:
509: default:
510: browser = "netscape";
511: break;
512: }
513: return browser;
514: }
515:
516: /**
517: * Attempts to open the default web browser to the given URL.
518: * @param url The URL to open
519: * @throws IOException If the web browser could not be located or does not run
520: */
521: public static void openURL(String url) throws IOException {
522: if (!loadedWithoutErrors) {
523: throw new IOException("Exception in finding browser: "
524: + errorMessage);
525: }
526: Object browser = locateBrowser();
527: if (browser == null) {
528: throw new IOException("Unable to locate browser: "
529: + errorMessage);
530: }
531:
532: switch (jvm) {
533: case MRJ_2_0:
534: Object aeDesc = null;
535: try {
536: aeDesc = aeDescConstructor
537: .newInstance(new Object[] { url });
538: putParameter.invoke(browser, new Object[] {
539: keyDirectObject, aeDesc });
540: sendNoReply.invoke(browser, new Object[] {});
541: } catch (InvocationTargetException ite) {
542: throw new IOException(
543: "InvocationTargetException while creating AEDesc: "
544: + ite.getMessage());
545: } catch (IllegalAccessException iae) {
546: throw new IOException(
547: "IllegalAccessException while building AppleEvent: "
548: + iae.getMessage());
549: } catch (InstantiationException ie) {
550: throw new IOException(
551: "InstantiationException while creating AEDesc: "
552: + ie.getMessage());
553: } finally {
554: aeDesc = null; // Encourage it to get disposed if it was created
555: browser = null; // Ditto
556: }
557: break;
558: case MRJ_2_1:
559: Runtime.getRuntime().exec(
560: new String[] { (String) browser, url });
561: break;
562: case MRJ_3_0:
563: int[] instance = new int[1];
564: int result = ICStart(instance, 0);
565: if (result == 0) {
566: int[] selectionStart = new int[] { 0 };
567: byte[] urlBytes = url.getBytes();
568: int[] selectionEnd = new int[] { urlBytes.length };
569: result = ICLaunchURL(instance[0], new byte[] { 0 },
570: urlBytes, urlBytes.length, selectionStart,
571: selectionEnd);
572: if (result == 0) {
573: // Ignore the return value; the URL was launched successfully
574: // regardless of what happens here.
575: ICStop(instance);
576: } else {
577: throw new IOException("Unable to launch URL: "
578: + result);
579: }
580: } else {
581: throw new IOException(
582: "Unable to create an Internet Config instance: "
583: + result);
584: }
585: break;
586: case MRJ_3_1:
587: try {
588: openURL.invoke(null, new Object[] { url });
589: } catch (InvocationTargetException ite) {
590: throw new IOException(
591: "InvocationTargetException while calling openURL: "
592: + ite.getMessage());
593: } catch (IllegalAccessException iae) {
594: throw new IOException(
595: "IllegalAccessException while calling openURL: "
596: + iae.getMessage());
597: }
598: break;
599: case WINDOWS_NT:
600: case WINDOWS_9x:
601: // Add quotes around the URL to allow ampersands and other special
602: // characters to work.
603: Process process = Runtime.getRuntime().exec(
604: new String[] { (String) browser,
605: FIRST_WINDOWS_PARAMETER,
606: SECOND_WINDOWS_PARAMETER,
607: THIRD_WINDOWS_PARAMETER, '"' + url + '"' });
608: // This avoids a memory leak on some versions of Java on Windows.
609: // That's hinted at in <http://developer.java.sun.com/developer/qow/archive/68/>.
610: try {
611: process.waitFor();
612: process.exitValue();
613: } catch (InterruptedException ie) {
614: throw new IOException(
615: "InterruptedException while launching browser: "
616: + ie.getMessage());
617: }
618: break;
619: case OTHER:
620: // Assume that we're on Unix and that Netscape is installed
621:
622: // First, attempt to open the URL in a currently running session of Netscape
623: process = Runtime.getRuntime().exec(
624: new String[] {
625: (String) browser,
626: NETSCAPE_REMOTE_PARAMETER,
627: NETSCAPE_OPEN_PARAMETER_START + url
628: + NETSCAPE_OPEN_PARAMETER_END });
629: try {
630: int exitCode = process.waitFor();
631: if (exitCode != 0) { // if Netscape was not open
632: Runtime.getRuntime().exec(
633: new String[] { (String) browser, url });
634: }
635: } catch (InterruptedException ie) {
636: throw new IOException(
637: "InterruptedException while launching browser: "
638: + ie.getMessage());
639: }
640: break;
641: default:
642: // This should never occur, but if it does, we'll try the simplest thing possible
643: Runtime.getRuntime().exec(
644: new String[] { (String) browser, url });
645: break;
646: }
647: }
648:
649: /**
650: * Methods required for Mac OS X. The presence of native methods does not cause
651: * any problems on other platforms.
652: */
653: private native static int ICStart(int[] instance, int signature);
654:
655: private native static int ICStop(int[] instance);
656:
657: private native static int ICLaunchURL(int instance, byte[] hint,
658: byte[] data, int len, int[] selectionStart,
659: int[] selectionEnd);
660: }
|