Add various ways of launching a browser for HelpInfo links, add support for
changing the prefix for those links and better formatting of them, cleanup
comments.

Patch by: jat
Review by: rjrjr


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6691 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/TreeLogger.java b/dev/core/src/com/google/gwt/core/ext/TreeLogger.java
index c433524..e934823 100644
--- a/dev/core/src/com/google/gwt/core/ext/TreeLogger.java
+++ b/dev/core/src/com/google/gwt/core/ext/TreeLogger.java
@@ -37,6 +37,13 @@
     }
 
     /**
+     * @return the prefix to go before the link.
+     */
+    public String getPrefix() {
+      return "More info: ";
+    }
+
+    /**
      * @return a URL containing extra information about the problem, or null if
      *     none.
      */
diff --git a/dev/core/src/com/google/gwt/dev/DevModeBase.java b/dev/core/src/com/google/gwt/dev/DevModeBase.java
index 99e1a5d..90932c3 100644
--- a/dev/core/src/com/google/gwt/dev/DevModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/DevModeBase.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.TreeLogger.HelpInfo;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.Precompile.PrecompileOptionsImpl;
 import com.google.gwt.dev.cfg.ModuleDef;
@@ -921,11 +922,12 @@
 
   protected void launchURL(String url) throws UnableToCompleteException {
     /*
-     * TODO(jat): properly support launching arbitrary browsers; waiting on
-     * Freeland's work with BrowserScanner and the trunk merge to get it.
+     * TODO(jat): properly support launching arbitrary browsers -- need some
+     * UI API tweaks to support that.
      */
+    URL parsedUrl = null;
     try {
-      URL parsedUrl = new URL(url);
+      parsedUrl = new URL(url);
       String path = parsedUrl.getPath();
       String query = parsedUrl.getQuery();
       String hash = parsedUrl.getRef();
@@ -939,8 +941,9 @@
       if (hash != null) {
         path += '#' + hash;
       }
-      url = new URL(parsedUrl.getProtocol(), parsedUrl.getHost(),
-          parsedUrl.getPort(), path).toExternalForm();
+      parsedUrl = new URL(parsedUrl.getProtocol(), parsedUrl.getHost(),
+          parsedUrl.getPort(), path);
+      url = parsedUrl.toExternalForm();
     } catch (MalformedURLException e) {
       getTopLogger().log(TreeLogger.ERROR, "Invalid URL " + url, e);
       throw new UnableToCompleteException();
@@ -948,8 +951,24 @@
     System.err.println("Using a browser with the GWT Development Plugin, please browse to");
     System.err.println("the following URL:");
     System.err.println("  " + url);
+    final URL helpInfoUrl = parsedUrl;
     getTopLogger().log(TreeLogger.INFO,
-        "Waiting for browser connection to " + url, null);
+        "Waiting for browser connection to " + url, null, new HelpInfo() {
+          @Override
+          public String getAnchorText() {
+            return "Launch default browser";
+          }
+          
+          @Override
+          public String getPrefix() {
+            return "";
+          }
+
+          @Override
+          public URL getURL() {
+            return helpInfoUrl;
+          }
+        });
   }
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java b/dev/core/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java
index 5d29208..b6a6c22 100644
--- a/dev/core/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java
+++ b/dev/core/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java
@@ -20,6 +20,7 @@
 import com.google.gwt.dev.shell.CloseButton;
 import com.google.gwt.dev.shell.CloseButton.Callback;
 import com.google.gwt.dev.shell.log.SwingTreeLogger.LogEvent;
+import com.google.gwt.dev.util.BrowserLauncher;
 import com.google.gwt.dev.util.log.AbstractTreeLogger;
 import com.google.gwt.dev.util.log.CompositeTreeLogger;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
@@ -249,8 +250,6 @@
 
   String regexFilter;
 
-  private boolean autoScroll;
-
   private final JEditorPane details;
 
   private final TreeLogger logger;
@@ -273,6 +272,13 @@
   
   private boolean disconnected = false;
 
+  /**
+   * Create a Swing-based logger panel, with a tree section and a detail
+   * section.
+   * 
+   * @param maxLevel
+   * @param logFile
+   */
   public SwingLoggerPanel(TreeLogger.Type maxLevel, File logFile) {
     super(new BorderLayout());
     regexFilter = "";
@@ -425,6 +431,9 @@
     });
   }
 
+  /**
+   * Collapse all tree nodes.
+   */
   @SuppressWarnings("unchecked")
   public void collapseAll() {
     Enumeration<DefaultMutableTreeNode> children = root.postorderEnumeration();
@@ -437,12 +446,18 @@
     tree.invalidate();
   }
 
+  /**
+   * Show that the client connected to this logger has disconnected.
+   */
   public void disconnected() {
     disconnected  = true;
     tree.setBackground(DISCONNECTED_COLOR);
     tree.repaint();
   }
 
+  /**
+   * Expand all tree nodes.
+   */
   @SuppressWarnings("unchecked")
   public void expandAll() {
     Enumeration<DefaultMutableTreeNode> children = root.postorderEnumeration();
@@ -455,10 +470,9 @@
     tree.invalidate();
   }
 
-  public boolean getAutoScroll() {
-    return autoScroll;
-  }
-
+  /**
+   * @return the TreeLogger for this panel
+   */
   public TreeLogger getLogger() {
     return logger;
   }
@@ -467,9 +481,14 @@
     EventType eventType = event.getEventType();
     if (eventType == HyperlinkEvent.EventType.ACTIVATED) {
       URL url = event.getURL();
-      // TODO(jat): how best to display the URL?  We could either figure out
-      // how to run the user's browser, create a mini-browser in Swing, or just
-      // re-use the details pane as we do here, but this is rather poor.
+      try {
+        BrowserLauncher.browse(url.toExternalForm());
+        return;
+      } catch (Exception e) {
+        // if anything fails, fall-through to failsafe implementation
+      }
+      // As a last resort, just use the details pane to display the HTML, but
+      // this is rather poor.
       try {
         details.setPage(url);
       } catch (IOException e) {
@@ -478,6 +497,9 @@
     }
   }
 
+  /**
+   * @param node
+   */
   public void notifyChange(DefaultMutableTreeNode node) {
     treeModel.nodeChanged(node);
   }
@@ -488,10 +510,6 @@
     details.setText("");
   }
 
-  public void setAutoScroll(boolean autoScroll) {
-    this.autoScroll = autoScroll;
-  }
-
   /**
    * Sets a callback for handling a close request, which also makes the close
    * button visible.
diff --git a/dev/core/src/com/google/gwt/dev/shell/log/SwingTreeLogger.java b/dev/core/src/com/google/gwt/dev/shell/log/SwingTreeLogger.java
index dc29218..30aef66 100644
--- a/dev/core/src/com/google/gwt/dev/shell/log/SwingTreeLogger.java
+++ b/dev/core/src/com/google/gwt/dev/shell/log/SwingTreeLogger.java
@@ -179,8 +179,9 @@
         if (anchorText == null) {
           anchorText = url.toExternalForm();
         }
+        String prefix = helpInfo.getPrefix();
         if (url != null) {
-          sb.append("\nMore info: <a href=\"");
+          sb.append("<p>" + prefix + "<a href=\"");
           sb.append(url.toString());
           sb.append("\">");
           sb.append(anchorText);
diff --git a/dev/core/src/com/google/gwt/dev/util/BrowserLauncher.java b/dev/core/src/com/google/gwt/dev/util/BrowserLauncher.java
new file mode 100644
index 0000000..55bb6e2
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/BrowserLauncher.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.util;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/**
+ * Provides a platform and JDK-independent method of launching a browser
+ * given a URI.
+ * 
+ * <p>Portions derived from public-domain code at
+ * <pre>http://www.centerkey.com/java/browser/</pre>
+ */
+public class BrowserLauncher {
+
+  /**
+   * A browser launcher that uses JDK 1.6 Desktop.browse support.
+   */
+  private static class Jdk16Launcher extends ReflectiveLauncher {
+    
+    /**
+     * Create a Jdk16Launcher if supported.
+     * 
+     * @throws UnsupportedOperationException if not supported
+     */
+    public Jdk16Launcher() throws UnsupportedOperationException {
+      try {
+        Class<?> desktopClass = Class.forName("java.awt.Desktop");
+        browseMethod = desktopClass.getMethod("browse", URI.class);
+        Method factory = desktopClass.getMethod("getDesktop");
+        browseObject = factory.invoke(null);
+        return;
+      } catch (ClassNotFoundException e) {
+        // not on JDK 1.6, try other methods
+      } catch (NoSuchMethodException e) {
+        // not on JDK 1.6, try other methods
+      } catch (SecurityException e) {
+        // ignore, try other methods
+      } catch (IllegalArgumentException e) {
+        // ignore, try other methods
+      } catch (IllegalAccessException e) {
+        // ignore, try other methods
+      } catch (InvocationTargetException e) {
+        // ignore, try other methods
+      }
+      throw new UnsupportedOperationException("no JDK 1.6 Desktop.browse");
+    }
+
+    @Override
+    protected Object convertUrl(String url) throws URISyntaxException, MalformedURLException {
+      return new URL(url).toURI();  
+    }
+  }
+
+  private interface Launcher {
+    void browse(String url) throws IOException, URISyntaxException;      
+  }
+
+  /**
+   * Launch the default browser on Mac via FileManager openURL.
+   */
+  private static class MacLauncher extends ReflectiveLauncher {
+    
+    public MacLauncher() throws UnsupportedOperationException {
+      Throwable caught = null;
+      try {
+        Class<?> fileManager = Class.forName("com.apple.eio.FileManager");
+        browseMethod = fileManager.getMethod("openURL", String.class);
+        browseObject = null;
+        return;
+      } catch (SecurityException e) {
+        caught = e;
+      } catch (ClassNotFoundException e) {
+        caught = e;
+      } catch (NoSuchMethodException e) {
+        caught = e;
+      }
+      throw new UnsupportedOperationException("Can't get openURL", caught);
+    }
+  }
+
+  /**
+   * Interface for launching a URL in a browser, which uses reflection.
+   * 
+   * <p>Subclass must set browseObject and browseMethod appropriately.
+   */
+  private abstract static class ReflectiveLauncher implements Launcher {
+
+    protected Object browseObject;
+    protected Method browseMethod;
+
+    public void browse(String url) throws IOException, URISyntaxException {
+      Object arg = convertUrl(url);
+      Throwable caught = null;
+      try {
+        browseMethod.invoke(browseObject, arg);
+        return;
+      } catch (InvocationTargetException e) {
+        Throwable cause = e.getCause();
+        if (cause instanceof IOException) {
+          throw (IOException) cause;
+        }
+        caught = e;
+      } catch (IllegalAccessException e) {
+        caught = e;
+      }
+      throw new RuntimeException("Unexpected exception", caught);
+    }
+
+    /**
+     * Convert the URL into another form if required.  The default
+     * implementation simply returns the unmodified string.
+     * 
+     * @param url URL in string form
+     * @return the URL in the form needed for browseMethod
+     * @throws URISyntaxException
+     * @throws MalformedURLException 
+     */
+    protected Object convertUrl(String url) throws URISyntaxException,
+        MalformedURLException {
+      return url;
+    }
+  }
+
+  /**
+   * Launch a browser by searching for a browser executable on the path.
+   */
+  private static class UnixExecBrowserLauncher implements Launcher {
+    
+    private static final String[] browsers = {
+      "firefox", "opera", "konqueror", "chrome", "chromium", "epiphany",
+      "seamonkey", "mozilla", "netscape", "galeon", "kazehakase",
+    };
+    
+    private String browserExecutable;
+
+    /**
+     * Creates a launcher by searching for a suitable browser executable.
+     * Assumes the presence of the "which" command.
+     * 
+     * @throws UnsupportedOperationException if no suitable browser can be
+     *           found.
+     */
+    public UnixExecBrowserLauncher() throws UnsupportedOperationException {
+      for (String browser : browsers) {
+        try {
+          Process process = Runtime.getRuntime().exec(new String[] { "which",
+              browser});
+          if (process.waitFor() == 0) {
+            browserExecutable = browser;
+            return;
+          }
+        } catch (IOException e) {
+          // ignore, try next one
+        } catch (InterruptedException e) {
+          // ignore, try next one
+        }
+      }
+      throw new UnsupportedOperationException("no suitable browser found");
+    }
+
+    public void browse(String url) throws IOException, URISyntaxException {
+      Runtime.getRuntime().exec(new String[] { browserExecutable, url });
+      // TODO(jat): do we need to wait for it to exit and check exit status?
+      // That would be best for Firefox, but bad for some of the other browsers.
+    }
+  }
+  
+  /**
+   * Launch the default browser on Windows via the URL protocol handler.
+   */
+  private static class WindowsLauncher implements Launcher {
+
+    public void browse(String url) throws IOException, URISyntaxException {
+      Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
+      // TODO(jat): do we need to wait for it to exit and check exit status?
+    }
+  }
+  
+  private static Launcher launcher;
+
+  /**
+   * Browse to a given URI.
+   * 
+   * @param url
+   * @throws IOException
+   * @throws URISyntaxException 
+   */
+  public static void browse(String url) throws IOException, URISyntaxException {
+    if (launcher == null) {
+      findLauncher();
+    }
+    launcher.browse(url);
+  }
+
+  /**
+   * Main method so this can be run from the command line for testing.
+   * 
+   * @param args URL to launch
+   * @throws URISyntaxException 
+   * @throws IOException 
+   */
+  public static void main(String[] args) throws IOException,
+      URISyntaxException {
+    if (args.length == 0) {
+      System.err.println("Usage: BrowserLauncher url...");
+      System.exit(1);
+    }
+    for (String url : args) {
+      browse(url);
+    }
+  }
+  
+  /**
+   * Initialize launcher to an appropriate one for the current platform/JDK.
+   */
+  private static void findLauncher() {
+    try {
+      launcher = new Jdk16Launcher();
+      return;
+    } catch (UnsupportedOperationException e) {
+      // ignore and try other methods
+    }
+    String osName = System.getProperty("os.name");
+    if (osName.startsWith("Mac OS")) {
+      launcher = new MacLauncher();
+    } else if (osName.startsWith("Windows")) {
+      launcher = new WindowsLauncher();
+    } else {
+      launcher = new UnixExecBrowserLauncher();
+      // let UnsupportedOperationException escape to caller
+    }
+  }
+}