Browsing URLs and opening files

Submitted by davidc on Wed, 22/04/2009 - 14:50

Java 1.6 added a new class, java.awt.Desktop, that provides handy methods to launch a web browser to a given URL, or open a given file. However these methods are not available under earlier JREs. This article provides a cross-platform surrogate class that uses the Desktop methods where possible, and falls back to platform-dependent methods if not. It uses reflection extensively to avoid linking problems. It also provides additional *AndWarn() methods to show a JOptionPane error message upon failure.

Using java.awt.Desktop under Java 1.6

java.awt.Desktop is the most reliable method to open URLs and files, and is used where possible. In addition to abstracting away platform differences, it also hides environmental differences (e.g. using AppletContext.showDocument() when running in an applet).

We uses Desktop exclusively by reflection, so that our class can still load into a JRE where the Desktop class doesn't exist.

The procedure for using the Desktop class is:

  1. Locate the Desktop class by reflection.
  2. Call isDesktopSupported().
  3. Retrieve the instance with getDesktop().
  4. Optionally, check isSupported(action) - I choose to skip this and instead catch exceptions from the actual method calls, to avoid additional verbose reflection calls.
  5. Call the browse(URI) or open(File) methods.

If any of these steps except the last fail, we fall back to platform-specific methods. If the last step failed, the actual launch failed.

Using rundll32 under Windows

Under Windows, we fall back to using rundll32 via Runtime.exec() to execute DLL methods, which are:

  • url.dll method FileProtocolHandler to browse a URL.
  • shell32.dll method ShellExec_RunDLL to open a File.

Using com.apple.eio.FileManager under Mac OS X

Apple does not provide Java 1.6 except, I believe, on 64-bit Intel Macs. Thus we almost always have to fall back to a platform specific method.

Fortunately they provide a handy class com.apple.eio.FileManager containing an openURL(String) method.

We access the FileManager class by reflection, and use openUrl() both for browsing URLs and (using a file:// URL) for opening files.

Trying different executables under Unix

Finally under Unix, we must search for an appropriate executable. At the top of the class is a list of possible executables, which we test for one by one by executing the which command to see if they exist on the user's PATH.

For browsing URLs, we try a list of common web browsers.

For opening files, we first try the run-mailcap command provided on many systems, which searches /etc/mailcap for an appropriate viewer for the detected MIME-type. If this doesn't exist, we fall back to assuming it's a text file and use a pager.

JavaDoc

browse

public static void browse(java.net.URL url)
                   throws java.io.IOException


Launches the given URL and throws an exception if it couldn't be launched.



Parameters:
url - the URL to open
Throws:
java.io.IOException - if a browser couldn't be found or if the URL failed to launch




browseAndWarn

public static void browseAndWarn(java.net.URL url,
                                 java.awt.Component parentComponent)


Launches the given URL and shows a dialog to the user if a browser couldn't be found or if the URL failed to launch.



Parameters:
url - the URL to open
parentComponent - The parent Component over which the error dialog should be shown




browseAndWarn

public static void browseAndWarn(java.lang.String url,
                                 java.awt.Component parentComponent)


Launches the given URL and shows a dialog to the user if a browser couldn't be found or if the URL failed to launch.
This version takes a String and handles the warning of malformed URLs where needed.



Parameters:
url - the URL to open
parentComponent - The parent Component over which the error dialog should be shown




open

public static void open(java.io.File file)
                 throws java.io.IOException


Opens the given File in the system default viewer application.



Parameters:
file - the File to open
Throws:
java.io.IOException - if an application couldn't be found or if the File failed to launch




openAndWarn

public static void openAndWarn(java.io.File file,
                               java.awt.Component parentComponent)


Launches the given URL and shows a dialog to the user if a browser couldn't be found or if the URL failed to launch.



Parameters:
file - the File to open
parentComponent - The parent Component over which the error dialog should be shown


The class

Here's the code. You can also download the full .java file at the bottom of the page.

package net.infotrek.util;
 
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
 
/**
 * Surrogate for the 1.6 java.awt.Desktop "browse(url)" and "open(file)" methods. Provides platform-dependent fallback methods
 * where the Desktop class is unavailable.
 * <p/>
 * Copyright (c) 2009 David C A Croft.
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author David Croft (http://www.davidc.net/)
 * @version $Id: DesktopUtil.java 262 2009-04-22 13:47:21Z david $
 */
@SuppressWarnings({"HardCodedStringLiteral", "DuplicateStringLiteralInspection", "StringConcatenation", "HardcodedLineSeparator"})
public class DesktopUtil
{
  private static final Logger log = Logger.getLogger(DesktopUtil.class.getName());
 
  private static final String OS_MACOS = "Mac OS";
  private static final String OS_WINDOWS = "Windows";
 
  private static final String[] UNIX_BROWSE_CMDS = {
          "www-browser", // debian update-alternatives target
          "firefox", "opera", "konqueror", "epiphany", "mozilla", "netscape", "w3m", "lynx"};
 
  private static final String[] UNIX_OPEN_CMDS = {
          "run-mailcap", // many Unixes, run registered program from /etc/mailcap
          // Fall back to assuming it's a text file.
          "pager", // debian update-alternatives target
          "less", "more"};
 
 
  private DesktopUtil()
  {
  }
 
  /**
   * Launches the given URL and throws an exception if it couldn't be launched.
   *
   * @param url the URL to open
   * @throws IOException if a browser couldn't be found or if the URL failed to launch
   */
  public static void browse(final URL url) throws IOException
  {
    // Try Java 1.6 Desktop class if supported
    if (browseDesktop(url)) return;
 
    final String osName = System.getProperty("os.name");
    log.finer("Launching " + url + " for OS " + osName);
 
    if (osName.startsWith(OS_MACOS)) {
      browseMac(url);
    }
    else if (osName.startsWith(OS_WINDOWS)) {
      browseWindows(url);
    }
    else {
      //assume Unix or Linux
      browseUnix(url);
    }
  }
 
  /**
   * Launches the given URL and shows a dialog to the user if a browser couldn't be found or if the URL failed to launch.
   *
   * @param url             the URL to open
   * @param parentComponent The parent Component over which the error dialog should be shown
   */
  public static void browseAndWarn(final URL url, final Component parentComponent)
  {
    try {
      browse(url);
    }
    catch (final IOException e) {
      log.log(Level.SEVERE, "Unable to browse to " + url, e);
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          JOptionPane.showMessageDialog(parentComponent, "Couldn't open a web browser:\n" + e.getLocalizedMessage(),
                  "Unable to launch web browser", JOptionPane.ERROR_MESSAGE);
        }
      });
    }
  }
 
  /**
   * Launches the given URL and shows a dialog to the user if a browser couldn't be found or if the URL failed to launch.
   * This version takes a String and handles the warning of malformed URLs where needed.
   *
   * @param url             the URL to open
   * @param parentComponent The parent Component over which the error dialog should be shown
   */
  public static void browseAndWarn(final String url, final Component parentComponent)
  {
    try {
      browse(new URL(url));
    }
    catch (final IOException e) {
      log.log(Level.SEVERE, "Unable to browse to " + url, e);
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          JOptionPane.showMessageDialog(parentComponent, "Couldn't open a web browser:\n" + e.getLocalizedMessage(),
                  "Unable to launch web browser", JOptionPane.ERROR_MESSAGE);
        }
      });
    }
  }
 
  /**
   * Opens the given File in the system default viewer application.
   *
   * @param file the File to open
   * @throws IOException if an application couldn't be found or if the File failed to launch
   */
  public static void open(final File file) throws IOException
  {
    // Try Java 1.6 Desktop class if supported
    if (openDesktop(file)) return;
 
    final String osName = System.getProperty("os.name");
    log.finer("Opening " + file + " for OS " + osName);
 
    if (osName.startsWith(OS_MACOS)) {
      openMac(file);
    }
    else if (osName.startsWith(OS_WINDOWS)) {
      openWindows(file);
    }
    else {
      //assume Unix or Linux
      openUnix(file);
    }
  }
 
  /**
   * Launches the given URL and shows a dialog to the user if a browser couldn't be found or if the URL failed to launch.
   *
   * @param file            the File to open
   * @param parentComponent The parent Component over which the error dialog should be shown
   */
  public static void openAndWarn(final File file, final Component parentComponent)
  {
    try {
      open(file);
    }
    catch (final IOException e) {
      log.log(Level.SEVERE, "Unable to open " + file, e);
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          JOptionPane.showMessageDialog(parentComponent, "Couldn't open " + file + ":\n" + e.getLocalizedMessage(),
                  "Unable to open file", JOptionPane.ERROR_MESSAGE);
        }
      });
    }
  }
 
  /**
   * Attempt to use java.awt.Desktop by reflection. Does not link directly to Desktop class so that this class can still
   * be loaded in JRE < 1.6.
   *
   * @param url the URL to launch
   * @return true if launch successful, false if we should fall back to other methods
   * @throws IOException if Desktop was found, but the browse() call failed.
   */
  private static boolean browseDesktop(final URL url) throws IOException
  {
    final Class desktopClass = getDesktopClass();
    if (desktopClass == null) return false;
 
    final Object desktopInstance = getDesktopInstance(desktopClass);
    if (desktopInstance == null) return false;
 
    log.finer("Launching " + url + " using Desktop.browse()");
 
    try {
      final Method browseMethod = desktopClass.getDeclaredMethod("browse", URI.class);
      browseMethod.invoke(desktopInstance, new URI(url.toExternalForm()));
      return true;
    }
    catch (InvocationTargetException e) {
      if (e.getCause() instanceof IOException) {
        throw (IOException) e.getCause();
      }
      else {
        log.log(Level.FINE, "Exception in Desktop operation", e);
        return false;
      }
    }
    catch (Exception e) {
      log.log(Level.FINE, "Exception in Desktop operation", e);
      return false;
    }
  }
 
  /**
   * Uses url.dll to browse to a URL under Windows.
   *
   * @param url the URL to launch
   * @throws IOException if the launch failed
   */
  private static void browseWindows(final URL url) throws IOException
  {
    log.finer("Windows invoking rundll32");
    Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url.toString()});
  }
 
  /**
   * Attempts to locate a browser from a predefined list under Unix.
   *
   * @param url the URL to launch
   * @throws IOException if the launch failed
   */
  private static void browseUnix(final URL url) throws IOException
  {
    for (final String cmd : UNIX_BROWSE_CMDS) {
      log.finest("Unix looking for " + cmd);
      if (unixCommandExists(cmd)) {
        log.finer("Unix found " + cmd);
        Runtime.getRuntime().exec(new String[]{cmd, url.toString()});
        return;
      }
    }
    throw new IOException("Could not find a suitable web browser");
  }
 
  /**
   * Attempt to use com.apple.eio.FileManager by reflection.
   *
   * @param url the URL to launch
   * @throws IOException if the launch failed
   */
  private static void browseMac(final URL url) throws IOException
  {
    try {
      final Class fileMgr = getAppleFileManagerClass();
      final Method openURL = fileMgr.getDeclaredMethod("openURL", String.class);
 
      log.finer("Mac invoking");
      openURL.invoke(null, url.toString());
    }
    catch (Exception e) {
      log.log(Level.WARNING, "Couldn't launch Mac URL", e);
      throw new IOException("Could not launch Mac URL: " + e.getLocalizedMessage());
    }
  }
 
 
  /**
   * Attempt to use java.awt.Desktop by reflection. Does not link directly to Desktop class so that this class can still
   * be loaded in JRE < 1.6.
   *
   * @param file the File to open
   * @return true if open successful, false if we should fall back to other methods
   * @throws IOException if Desktop was found, but the open() call failed.
   */
  private static boolean openDesktop(final File file) throws IOException
  {
    final Class desktopClass = getDesktopClass();
    if (desktopClass == null) return false;
 
    final Object desktopInstance = getDesktopInstance(desktopClass);
    if (desktopInstance == null) return false;
 
    log.finer("Opening " + file + " using Desktop.open()");
 
    try {
      final Method browseMethod = desktopClass.getDeclaredMethod("open", File.class);
      browseMethod.invoke(desktopInstance, file);
      return true;
    }
    catch (InvocationTargetException e) {
      if (e.getCause() instanceof IOException) {
        throw (IOException) e.getCause();
      }
      else if (e.getCause() instanceof IllegalArgumentException) {
        throw new FileNotFoundException(e.getCause().getLocalizedMessage());
      }
      else {
        log.log(Level.FINE, "Exception in Desktop operation", e);
        return false;
      }
    }
    catch (Exception e) {
      log.log(Level.FINE, "Exception in Desktop operation", e);
      return false;
    }
  }
 
  /**
   * Uses shell32.dll to open a file under Windows.
   *
   * @param file the File to open
   * @throws IOException if the open failed
   */
  private static void openWindows(final File file) throws IOException
  {
    log.finer("Windows invoking rundll32");
    Runtime.getRuntime().exec(new String[]{"rundll32", "shell32.dll,ShellExec_RunDLL", file.getAbsolutePath()});
  }
 
  /**
   * Attempt to use com.apple.eio.FileManager by reflection.
   *
   * @param file the File to open
   * @throws IOException if the open failed
   */
  private static void openMac(final File file) throws IOException
  {
    // we use openURL() on the file's URL form since openURL supports file:// protocol
    browseMac(file.getAbsoluteFile().toURL());
  }
 
 
  /**
   * Attempts to locate a viewer from a predefined list under Unix.
   *
   * @param file the File to open
   * @throws IOException if the open failed
   */
  private static void openUnix(final File file) throws IOException
  {
    for (final String cmd : UNIX_OPEN_CMDS) {
      log.finest("Unix looking for " + cmd);
      if (unixCommandExists(cmd)) {
        log.finer("Unix found " + cmd);
        Runtime.getRuntime().exec(new String[]{cmd, file.getAbsolutePath()});
        return;
      }
    }
    throw new IOException("Could not find a suitable viewer");
  }
 
  /**
   * Find the Desktop class if it exists in this JRE.
   *
   * @return the Desktop class object, or null if it could not be found
   */
  private static Class getDesktopClass()
  {
    // NB The following String is intentionally not inlined to prevent ProGuard trying to locate the unknown class.
    final String desktopClassName = "java.awt.Desktop";
    try {
      return Class.forName(desktopClassName);
    }
    catch (ClassNotFoundException e) {
      log.fine("Desktop class not found");
      return null;
    }
  }
 
  /**
   * Gets a Desktop class instance if supported. We check isDesktopSupported() but for convenience we don't bother to
   * check isSupported(method); instead the caller handles any UnsupportedOperationExceptions.
   *
   * @param desktopClass the Desktop Class object
   * @return the Desktop instance, or null if it is not supported
   */
  private static Object getDesktopInstance(final Class desktopClass)
  {
    try {
      final Method isDesktopSupportedMethod = desktopClass.getDeclaredMethod("isDesktopSupported");
      log.finest("invoking isDesktopSupported");
      final boolean isDesktopSupported = (Boolean) isDesktopSupportedMethod.invoke(null);
 
      if (!isDesktopSupported) {
        log.finer("isDesktopSupported: no");
        return null;
      }
 
      final Method getDesktopMethod = desktopClass.getDeclaredMethod("getDesktop");
      return getDesktopMethod.invoke(null);
    }
    catch (Exception e) {
      log.log(Level.FINE, "Exception in Desktop operation", e);
      return null;
    }
  }
 
  /**
   * Finds the com.apple.eio.FileManager class on a Mac.
   *
   * @return the FileManager instance
   * @throws ClassNotFoundException if FileManager was not found
   */
  private static Class getAppleFileManagerClass() throws ClassNotFoundException
  {
    log.finest("Mac looking for com.apple.eio.FileManager");
 
    // NB The following String is intentionally not inlined to prevent ProGuard trying to locate the unknown class.
    final String appleClass = "com.apple.eio.FileManager";
    return Class.forName(appleClass);
  }
 
  /**
   * Checks whether a given executable exists, by means of the "which" command.
   *
   * @param cmd the executable to locate
   * @return true if the executable was found
   * @throws IOException if Runtime.exec() throws an IOException
   */
  private static boolean unixCommandExists(final String cmd) throws IOException
  {
    final Process whichProcess = Runtime.getRuntime().exec(new String[]{"which", cmd});
 
    boolean finished = false;
    do {
      try {
        whichProcess.waitFor();
        finished = true;
      }
      catch (InterruptedException e) {
        log.log(Level.WARNING, "Interrupted waiting for which to complete", e);
      }
    } while (!finished);
 
    return whichProcess.exitValue() == 0;
  }
 
  @SuppressWarnings({"HardcodedFileSeparator"})
  public static void main(final String[] args) throws IOException
  {
    Logger.getLogger("").getHandlers()[0].setLevel(Level.ALL);
 
    DesktopUtil.browseAndWarn(new URL("http://goofans.com/"), null);
    DesktopUtil.openAndWarn(new File("dist_src/win32/lib/irrKlang/README.txt"), null);
//    DesktopUtil.browseAndWarn(new File("dist_src/win32/lib/irrKlang/README.txt").getAbsoluteFile().toURL(), null);
  }
}

Attachment Size
DesktopUtil.java 15.57 KB