/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.desktop.ostermiller;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.MessageFormat;
import java.util.Vector;

/**
 * A stripped down version of the Browser class published by Stephen Ostermiller for use by the OSP project.
 *
 * Allows URLs to be opened in the system browser on Windows and Unix.
 * More information about this class is available from <a target="_top" href=
 * "http://ostermiller.org/utils/Browser.html">ostermiller.org</a>.
 *
 * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities
 * @since ostermillerutils 1.00.00
 */
public class Browser {
  /**
   * A list of commands to try in order to display the url.
   * The url is put into the command using MessageFormat, so
   * the URL will be specified as {0} in the command.
   * Some examples of commands to try might be:<br>
   * <code>rundll32 url.dll,FileProtocolHandler {0}</code></br>
   * <code>netscape {0}</code><br>
   * These commands are passed in order to exec until something works
   * when displayURL is used.
   *
   * @since ostermillerutils 1.00.00
   */
  public static String[] exec = null;

  /**
   * Determine appropriate commands to start a browser on the current
   * operating system.  On windows: <br>
   * <code>rundll32 url.dll,FileProtocolHandler {0}</code></br>
   * On other operating systems, the "which" command is used to
   * test if Mozilla, netscape, and lynx(xterm) are available (in that
   * order).
   *
   * @since ostermillerutils 1.00.00
   */
  public static void init() {
    exec = defaultCommands();
  }

  /**
   * Retrieve the default commands to open a browser for this system.
   *
   * @since ostermillerutils 1.00.00
   */
  public static String[] defaultCommands() {
    String[] exec = null;
    if(System.getProperty("os.name").startsWith("Windows")) {      //$NON-NLS-1$ //$NON-NLS-2$
      exec = new String[] {
        "rundll32 url.dll,FileProtocolHandler {0}",                //$NON-NLS-1$
      };
    } else if(System.getProperty("os.name").startsWith("Mac")) {   //$NON-NLS-1$ //$NON-NLS-2$
      Vector<String> browsers = new Vector<String>();
      try {
        Process p = Runtime.getRuntime().exec("which open");       //$NON-NLS-1$
        if(p.waitFor()==0) {
          browsers.add("open {0}");                                //$NON-NLS-1$
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      if(browsers.size()>0) {
        exec = browsers.toArray(new String[0]);
      }
    } else if(System.getProperty("os.name").startsWith("SunOS")) { //$NON-NLS-1$ //$NON-NLS-2$
      exec = new String[] {"/usr/dt/bin/sdtwebclient {0}"};        //$NON-NLS-1$
    } else {
      Vector<String> browsers = new Vector<String>();
      try {
        Process p = Runtime.getRuntime().exec("which firebird");   //$NON-NLS-1$
        if(p.waitFor()==0) {
          browsers.add("firebird -remote openURL({0})");           //$NON-NLS-1$
          browsers.add("firebird {0}");                            //$NON-NLS-1$
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      try {
        Process p = Runtime.getRuntime().exec("which mozilla");    //$NON-NLS-1$
        if(p.waitFor()==0) {
          browsers.add("mozilla -remote openURL({0})");            //$NON-NLS-1$
          browsers.add("mozilla {0}");                             //$NON-NLS-1$
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      try {
        Process p = Runtime.getRuntime().exec("which opera");      //$NON-NLS-1$
        if(p.waitFor()==0) {
          browsers.add("opera -remote openURL({0})");              //$NON-NLS-1$
          browsers.add("opera {0}");                               //$NON-NLS-1$
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      try {
        Process p = Runtime.getRuntime().exec("which galeon");     //$NON-NLS-1$
        if(p.waitFor()==0) {
          browsers.add("galeon {0}");                              //$NON-NLS-1$
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      try {
        Process p = Runtime.getRuntime().exec("which konqueror");  //$NON-NLS-1$
        if(p.waitFor()==0) {
          browsers.add("konqueror {0}");                           //$NON-NLS-1$
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      try {
        Process p = Runtime.getRuntime().exec("which netscape");   //$NON-NLS-1$
        if(p.waitFor()==0) {
          browsers.add("netscape -remote openURL({0})");           //$NON-NLS-1$
          browsers.add("netscape {0}");                            //$NON-NLS-1$
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      try {
        Process p = Runtime.getRuntime().exec("which xterm");      //$NON-NLS-1$
        if(p.waitFor()==0) {
          p = Runtime.getRuntime().exec("which lynx");             //$NON-NLS-1$
          if(p.waitFor()==0) {
            browsers.add("xterm -e lynx {0}");                     //$NON-NLS-1$
          }
        }
      } catch(IOException e) {}
      catch(InterruptedException e) {}
      if(browsers.size()>0) {
        exec = browsers.toArray(new String[0]);
      }
    }
    return exec;
  }

  /**
   * Display a URL in the system browser.
   *
   * Browser.init() should be called before calling this function or
   * Browser.exec should be set explicitly.
   *
   * For security reasons, the URL will may not be passed directly to the
   * browser as it is passed to this method.  The URL may be made safe for
   * the exec command by URLEncoding the URL before passing it.
   *
   * @param url the url to display
   * @throws IOException if the url is not valid or the browser fails to star
   *
   * @since ostermillerutils 1.00.00
   */
  public static void displayURL(String url) throws IOException {
    if((exec==null)||(exec.length==0)) {
      if(System.getProperty("os.name").startsWith("Mac")) {                                             //$NON-NLS-1$ //$NON-NLS-2$
        boolean success = false;
        try {
          Class<?> nSWorkspace;
          if(new File("/System/Library/Java/com/apple/cocoa/application/NSWorkspace.class").exists()) { //$NON-NLS-1$
            // Mac OS X has NSWorkspace, but it is not in the classpath, add it.
            //ClassLoader classLoader = new URLClassLoader(new URL[]{new File("/System/Library/Java").toURL()}); //$NON-NLS-1$
            ClassLoader classLoader = new URLClassLoader(new URL[] {new File("/System/Library/Java").toURI().toURL()}); //$NON-NLS-1$
            nSWorkspace = Class.forName("com.apple.cocoa.application.NSWorkspace", true, classLoader);    //$NON-NLS-1$
          } else {
            nSWorkspace = Class.forName("com.apple.cocoa.application.NSWorkspace");                       //$NON-NLS-1$
          }
          Method sharedWorkspace = nSWorkspace.getMethod("sharedWorkspace", new Class[] {});              //$NON-NLS-1$
          Object workspace = sharedWorkspace.invoke(null, new Object[] {});
          Method openURL = nSWorkspace.getMethod("openURL", new Class[] {Class.forName("java.net.URL")}); //$NON-NLS-1$ //$NON-NLS-2$
          success = ((Boolean) openURL.invoke(workspace, new Object[] {new java.net.URL(url)})).booleanValue();
          //success = com.apple.cocoa.application.NSWorkspace.sharedWorkspace().openURL(new java.net.URL(url));
        } catch(Exception x) {}
        if(!success) {
          try {
            Class<?> mrjFileUtils = Class.forName("com.apple.mrj.MRJFileUtils");                                 //$NON-NLS-1$
            Method openURL = mrjFileUtils.getMethod("openURL", new Class[] {Class.forName("java.lang.String")}); //$NON-NLS-1$ //$NON-NLS-2$
            openURL.invoke(null, new Object[] {url});
            //com.apple.mrj.MRJFileUtils.openURL(url);
          } catch(Exception x) {
            System.err.println(x.getMessage());
            throw new IOException("Browser launch failed.");           //$NON-NLS-1$
          }
        }
      } else {
        throw new IOException("Browser execute command not defined."); //$NON-NLS-1$
      }
    } else {
      // for security, see if the url is valid.
      // this is primarily to catch an attack in which the url
      // starts with a - to fool the command line flags, bu
      // it could catch other stuff as well, and will throw a
      // MalformedURLException which will give the caller of this
      // function useful information.
      new URL(url);
      // escape any weird characters in the url.  This is primarily
      // to prevent an attacker from putting in spaces
      // that might fool exec into allowing
      // the attacker to execute arbitrary code.
      StringBuffer sb = new StringBuffer(url.length());
      for(int i = 0; i<url.length(); i++) {
        char c = url.charAt(i);
        if(((c>='a')&&(c<='z'))||((c>='A')&&(c<='Z'))||((c>='0')&&(c<='9'))||(c=='.')||(c==':')||(c=='&')||(c=='@')||(c=='/')||(c=='?')||(c=='%')||(c=='+')||(c=='=')||(c=='#')||(c=='-')||(c=='\\')) {
          //characters that are necessary for URLs and should be safe
          //to pass to exec.  Exec uses a default string tokenizer with
          //the default arguments (whitespace) to separate command line
          //arguments, so there should be no problem with anything bu
          //whitespace.
          sb.append(c);
        } else {
          c = (char) (c&0xFF);                                                                           // get the lowest 8 bits (URLEncoding)
          if(c<0x10) {
            sb.append("%0"+Integer.toHexString(c));                                                      //$NON-NLS-1$
          } else {
            sb.append("%"+Integer.toHexString(c));                                                       //$NON-NLS-1$
          }
        }
      }
      String[] messageArray = new String[1];
      messageArray[0] = sb.toString();
      String command = null;
      boolean found = false;
      // try each of the exec commands until something works
      try {
        for(int i = 0; (i<exec.length)&&!found; i++) {
          try {
            // stick the url into the command
            command = MessageFormat.format(exec[i], (Object[]) messageArray);
            // parse the command line.
            Vector<String> argsVector = new Vector<String>();
            BrowserCommandLexer lex = new BrowserCommandLexer(new StringReader(command));
            String t;
            while((t = lex.getNextToken())!=null) {
              argsVector.add(t);
            }
            String[] args = new String[argsVector.size()];
            args = argsVector.toArray(args);
            // the windows url protocol handler doesn't work well with file URLs.
            // Correct those problems here before continuing
            // Java File.toURL() gives only one / following file: bu
            // we need two.
            // If there are escaped characters in the url, we will have
            // to create an Internet shortcut and open that, as the command
            // line version of the rundll doesn't like them.
            if(args[0].equals("rundll32")&&args[1].equals("url.dll,FileProtocolHandler")) {              //$NON-NLS-1$ //$NON-NLS-2$
              if(args[2].startsWith("file:/")) {                                                         //$NON-NLS-1$
                if(args[2].charAt(6)!='/') {
                  args[2] = "file://"+args[2].substring(6);                                              //$NON-NLS-1$
                }
                if(args[2].charAt(7)!='/') {
                  args[2] = "file:///"+args[2].substring(7);                                             //$NON-NLS-1$
                }
              } else if(args[2].toLowerCase().endsWith("html")||args[2].toLowerCase().endsWith("htm")) { //$NON-NLS-1$ //$NON-NLS-2$
              }
            }
            // start the browser
            Process p = Runtime.getRuntime().exec(args);
            // give the browser a bit of time to fail.
            // I have found that sometimes sleep doesn't work
            // the first time, so do it twice.  My tests
            // seem to show that 1000 milliseconds is enough
            // time for the browsers I'm using.
            for(int j = 0; j<2; j++) {
              try {
                Thread.sleep(1000);
              } catch(InterruptedException inte) {}
            }
            if(p.exitValue()==0) {
              // this is a weird case.  The browser exited after
              // a couple seconds saying that it successfully
              // displayed the url.  Either the browser is lying
              // or the user closed it *really* quickly.  Oh well.
              found = true;
            }
          } catch(IOException x) {
            // the command was not a valid command.
            System.err.println("Warning: "+x.getMessage());                 //$NON-NLS-1$
          }
        }
        if(!found) {
          // we never found a command that didn't terminate with an error.
          throw new IOException("Browser launch failed.");                  //$NON-NLS-1$
        }
      } catch(IllegalThreadStateException e) {
        // the browser is still running.  This is a good sign.
        // lets just say that it is displaying the url right now!
      }
    }
  }

  /**
   * Display the URLs, each in their own window, in the system browser.
   *
   * Browser.init() should be called before calling this function or
   * Browser.exec should be set explicitly.
   *
   * If more than one URL is given an HTML page containing JavaScript will
   * be written to the local drive, that page will be opened, and it will
   * open the rest of the URLs.
   *
   * @param urls the list of urls to display
   * @throws IOException if the url is not valid or the browser fails to star
   *
   * @since ostermillerutils 1.00.00
   */
  public static void displayURLs(String[] urls) throws IOException {
    if((urls==null)||(urls.length==0)) {
      return;
    }
    if(urls.length==1) {
      displayURL(urls[0]);
      return;
    }
    File shortcut = File.createTempFile("DisplayURLs", ".html"); //$NON-NLS-1$ //$NON-NLS-2$
    shortcut = shortcut.getCanonicalFile();
    shortcut.deleteOnExit();
    PrintWriter out = new PrintWriter(new FileWriter(shortcut));
    out.println("<!-- saved from url=(0014)about:internet -->");              //$NON-NLS-1$
    out.println("<html>");                                                    //$NON-NLS-1$
    out.println("<head>");                                                    //$NON-NLS-1$
    out.println("<title> Open URLs </title>");                                //$NON-NLS-1$
    out.println("<script language=\"javascript\" type=\"text/javascript\">"); //$NON-NLS-1$
    out.println("function displayURLs(){");                                   //$NON-NLS-1$
    for(int i = 1; i<urls.length; i++) {
      out.println("window.open(\""+urls[i]+"\", \"_blank\", \"toolbar=yes,location=yes,directories=yes,status=yes,menubar=yes,scrollbars=yes,resizable=yes\");"); //$NON-NLS-1$ //$NON-NLS-2$
    }
    out.println("location.href=\""+urls[0]+"\";");             //$NON-NLS-1$ //$NON-NLS-2$
    out.println("}");                                          //$NON-NLS-1$
    out.println("</script>");                                  //$NON-NLS-1$
    out.println("</head>");                                    //$NON-NLS-1$
    out.println("<body onload=\"javascript:displayURLs()\">"); //$NON-NLS-1$
    out.println("<noscript>");                                 //$NON-NLS-1$
    for(int i = 0; i<urls.length; i++) {
      out.println("<a target=\"_blank\" href=\""+urls[i]+"\">"+urls[i]+"</a><br>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    }
    out.println("</noscript>"); //$NON-NLS-1$
    out.println("</body>");     //$NON-NLS-1$
    out.println("</html>");     //$NON-NLS-1$
    out.close();
    displayURL(shortcut.toURI().toURL().toString());
  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
