/*
 * 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.tools;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import org.opensourcephysics.controls.ConsoleLevel;
import org.opensourcephysics.controls.OSPLog;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.controls.XMLControlElement;
import org.opensourcephysics.controls.XMLLoader;
import org.opensourcephysics.display.OSPLayout;
import org.opensourcephysics.display.OSPRuntime;

/**
 * This is a tree node that can describe and launch an application.
 *
 * @author Douglas Brown
 * @version 1.0
 */
public class LaunchNode extends DefaultMutableTreeNode {
  // static fields
  static final Level DEFAULT_LOG_LEVEL = ConsoleLevel.OUT_CONSOLE;
  // instance fields
  Object launchObj;                                            // object providing xml data when arg 0 is "this"
  String classPath;                                            // relative paths to jar files with launch/support classes
  String launchClassName;                                      // name of class to be launched
  Class<?> launchClass;                                        // class to be launched
  String[] args = new String[] {""};                           // args passed to main method when launching //$NON-NLS-1$
  boolean showLog = false;                                     // shows OSPLog when in single vm
  boolean clearLog = false;                                    // clears OSPLog if shown
  Level logLevel = DEFAULT_LOG_LEVEL;                          // OSPLog set to this level before log is shown
  boolean singleVM = false;                                    // launches in current vm
  boolean singleVMOff = false;                                 // allows child to override single vm of parent
  boolean hiddenWhenRoot = false;                              // hides node when at root position
  boolean buttonView = false;                                  // displays node in button view when root
  boolean singleton = false;                                   // allows single instance when in separate vm
  boolean singleApp = false;                                   // allows single app when in single vm
  boolean singleAppOff = false;                                // allows child to override single app of parent
  boolean hiddenInLauncher = false;                            // hides node in Launcher (not LaunchBuilder)
  String name = "";                                            // display name of this node //$NON-NLS-1$
  String description = "";                                     // description displayed if no url specified or found //$NON-NLS-1$
  String tooltip = "";                                         // tooltip for this node //$NON-NLS-1$
  String xsetName = "";                                        // name of xset to be loaded (splash) //$NON-NLS-1$
  String author = "";                                          // author of the curricular activity defined by this node //$NON-NLS-1$
  String keywords = "";                                        // key words associated with this node //$NON-NLS-1$
  String level = "";                                           // complexity level for this node //$NON-NLS-1$
  String languages = "";                                       // languages associated with this node //$NON-NLS-1$
  String comment = "";                                         // comments about this node //$NON-NLS-1$
  String appletWidth = "";                                     // preferred applet width //$NON-NLS-1$
  String appletHeight = "";                                    // preferred applet height //$NON-NLS-1$
  ArrayList<DisplayTab> tabData = new ArrayList<DisplayTab>(); // list of display tabs
  private String fileName;                                     // xml file name relative to tabset base
  Collection<Process> processes = new HashSet<Process>();      // processes launched by this node
  Collection<Frame> frames = new HashSet<Frame>();             // frames launched by this node
  Collection<Action> actions = new HashSet<Action>();          // terminateActions to be performed by this node
  Map<Runnable, Thread> threads = new HashMap<Runnable, Thread>();
  int launchCount = 0;
  LaunchPanel launchPanel;
  boolean selfContained;
  boolean parentSelfContained;
  boolean previewing;
  boolean saveHiddenNodes;
  boolean enabled = true;
  List<String> jars = new ArrayList<String>();
  // following used for undoable NavEdits
  int tabNumber = -1;                                          // current tab number, or -1 if no tabs
  int prevTabNumber = -1;                                      // previous tab number
  URL htmlURL;                                                 // current URL--may be null
  URL prevURL;                                                 // previous URL
  JScrollPane launchModelScroller;

  /**
   * Constructs a node with the specified name.
   *
   * @param name the name
   */
  public LaunchNode(String name) {
    setUserObject(this);
    if(name!=null) {
      this.name = name;
    }
  }

  /**
   * Signals that a launch thread for this node is about to start or end.
   *
   * @param starting true if the thread is starting
   */
  public void threadRunning(boolean starting) {
    launchCount += starting ? +1 : -1;
    launchCount = Math.max(0, launchCount);
    if(launchPanel!=null) {
      launchPanel.repaint();
    }
  }

  /**
   * Launches this node.
   */
  public void launch() {
    launch(null);
  }

  /**
   * Launches this node from the specified launch panel.
   *
   * @param tab the launch panel
   */
  public void launch(LaunchPanel tab) {
    if(!isLeaf()) {
      return;
    }
    launchPanel = tab;
    OSPRuntime.launchingInSingleVM = this.isSingleVM();
    Launcher.singleAppMode = this.isSingleApp();
    Launcher.classPath = getClassPath(); // in node-to-root order
    if(isShowLog()&&isSingleVM()) {
      OSPLog.setLevel(getLogLevel());
      OSPLog log = OSPLog.getOSPLog();
      if(isClearLog()) {
        log.clear();
      }
      log.setVisible(true);
    }
    setMinimumArgLength(1); // trim args if nec
    String arg0 = args[0];
    if(getLaunchClass()!=null) {
      if(arg0.equals("this")) {                  //$NON-NLS-1$
        Object launchObj = getLaunchObject();
        if(launchObj!=null) {
          // replace with xml from launch object, if any
          XMLControl control = new XMLControlElement(launchObj);
          args[0] = control.toXML();
        } else {
          args[0] = "";                          //$NON-NLS-1$
        }
      }
      if(args[0].equals("")&&(args.length==1)) { //$NON-NLS-1$
        Launcher.launch(getLaunchClass(), null, this);
      } else {
        Launcher.launch(getLaunchClass(), args, this);
      }
    }
    args[0] = arg0;
  }

  /**
   * Returns the nearest ancestor with a non-null file name.
   * This node is returned if it has a non-null file name.
   *
   * @return the file node
   */
  public LaunchNode getOwner() {
    if(fileName!=null) {
      return this;
    }
    if(getParent()!=null) {
      return((LaunchNode) getParent()).getOwner();
    }
    return null;
  }

  /**
   * Returns all descendents of this node with non-null file names.
   * This node is not included.
   *
   * @return an array of launch nodes
   */
  public LaunchNode[] getAllOwnedNodes() {
    Collection<LaunchNode> nodes = new ArrayList<LaunchNode>();
    Enumeration<?> e = breadthFirstEnumeration();
    while(e.hasMoreElements()) {
      LaunchNode next = (LaunchNode) e.nextElement();
      if((next.fileName!=null)&&(next!=this)) {
        nodes.add(next);
      }
    }
    return nodes.toArray(new LaunchNode[0]);
  }

  /**
   * Returns the nearest descendents of this node with non-null file names.
   * This node is not included.
   *
   * @return an array of launch nodes
   */
  public LaunchNode[] getChildOwnedNodes() {
    Collection<LaunchNode> nodes = new ArrayList<LaunchNode>();
    LaunchNode[] owned = getAllOwnedNodes();
    LaunchNode owner = getOwner();
    for(int i = 0; i<owned.length; i++) {
      LaunchNode next = ((LaunchNode) owned[i].getParent()).getOwner();
      if(next==owner) {
        nodes.add(owned[i]);
      }
    }
    return nodes.toArray(new LaunchNode[0]);
  }

  /**
   * Returns a string used as a display name for this node.
   *
   * @return the string name of this node
   */
  public String toString() {
    // return name, if any
    if((name!=null)&&!name.equals("")) { //$NON-NLS-1$
      return name;
    }
    // return launch class name, if any
    if(launchClassName!=null) {
      return XML.getExtension(launchClassName);
    }
    // return args[0] name, if any
    if(!args[0].equals("")) { //$NON-NLS-1$
      String name = args[0];
      name = XML.getName(name);
      return XML.stripExtension(name);
    }
    return ""; //$NON-NLS-1$
  }

  /**
   * Gets the unique ID string for this node.
   *
   * @return the ID string
   */
  public String getID() {
    return String.valueOf(this.hashCode());
  }

  /**
   * Sets the name of this node.
   *
   * @param name the name
   */
  public void setName(String name) {
    this.name = name;
  }

  /**
   * Gets the name of this node.
   *
   * @return the name
   */
  public String getName() {
    return name;
  }

  /**
   * Sets the description of this node.
   *
   * @param desc the description
   */
  public void setDescription(String desc) {
    description = desc;
  }

  /**
   * Gets the description of this node.
   *
   * @return the description
   */
  public String getDescription() {
    return description;
  }

  /**
   * Sets the launch arguments of this node.
   *
   * @param args the arguments
   */
  public void setArgs(String[] args) {
    if((args!=null)&&(args.length>0)&&(args[0]!=null)) {
      this.args = args;
    }
  }

  /**
   * Gets the launch arguments of this node.
   *
   * @return the launch arguments
   */
  public String[] getArgs() {
    return args;
  }

  /**
   * Sets the tooltip of this node
   *
   * @param _tooltip the tooltip
   */
  public void setTooltip(String _tooltip) {
    tooltip = _tooltip;
  }

  /**
   * Gets the tooltip of this node.
   *
   * @return the tooltip
   */
  public String getTooltip() {
    return tooltip;
  }

  /**
   * Sets the author of this node
   *
   * @param _author the author
   */
  public void setAuthor(String _author) {
    author = _author;
  }

  /**
   * Gets the author.
   *
   * @return the first non-null author of this or an ancestor
   */
  public String getAuthor() {
    if(!author.equals("")) { //$NON-NLS-1$
      return author;
    } else if(isRoot()) {
      return "";             //$NON-NLS-1$
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.getAuthor();
  }

  /**
   * Sets the keywords of this node
   *
   * @param _keywords the keywords
   */
  public void setKeyword(String _keywords) {
    keywords = _keywords;
  }

  /**
   * Gets the keywords of this node or ancestor.
   *
   * @return the keywords
   */
  public String getKeywords() {
    if(!keywords.equals("")) { //$NON-NLS-1$
      return keywords;
    } else if(isRoot()) {
      return "";               //$NON-NLS-1$
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.getKeywords();
  }

  /**
   * Sets the comment of this node
   *
   * @param _comment the comment
   */
  public void setComment(String _comment) {
    comment = _comment;
  }

  /**
   * Gets the comment of this node.
   *
   * @return the comment
   */
  public String getComment() {
    return comment;
  }

  /**
   * Sets the preferred applet width of this node
   *
   * @param _width the width
   */
  public void setPreferredAppletWidth(String _width) {
    this.appletWidth = _width;
  }

  /**
   * Gets the preferred applet width of this node.
   *
   * @return the width
   */
  public String getPreferredAppletWidth() {
    return this.appletWidth;
  }

  /**
   * Sets the preferred applet height of this node
   *
   * @param _height the height
   */
  public void setPreferredAppletHeight(String _height) {
    this.appletHeight = _height;
  }

  /**
   * Gets the preferred applet height of this node.
   *
   * @return the height
   */
  public String getPreferredAppletHeight() {
    return this.appletHeight;
  }

  /**
   * Sets the course level of this node
   *
   * @param _level the level
   */
  public void setCourseLevel(String _level) {
    level = _level;
  }

  /**
   * Gets the course level of this node or ancestor.
   *
   * @return the level
   */
  public String getCourseLevel() {
    if(!level.equals("")) { //$NON-NLS-1$
      return level;
    } else if(isRoot()) {
      return "";            //$NON-NLS-1$
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.getCourseLevel();
  }

  /**
   * Sets the languages for which translations exist for this node
   *
   * @param _lang the languages
   */
  public void setLanguages(String _lang) {
    languages = _lang;
  }

  /**
   * Gets the languages for which translations exist for this node or ancestor.
   *
   * @return the languages
   */
  public String getLanguages() {
    if(!languages.equals("")) { //$NON-NLS-1$
      return languages;
    } else if(isRoot()) {
      return "";                //$NON-NLS-1$
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.getLanguages();
  }

  /**
   * Gets the complete class path in node-to-root order.
   * If Launcher is running from a jar, that jar is in every classpath.
   *
   * @return the class path
   */
  public String getClassPath() {
    String path = ""; //$NON-NLS-1$
    if(classPath!=null) {
      path += classPath;
    }
    LaunchNode node = this;
    while(!node.isRoot()) {
      node = (LaunchNode) node.getParent();
      if(node.classPath!=null) {
        if(!path.equals("")) { //$NON-NLS-1$
          path += ";";         //$NON-NLS-1$
        }
        path += node.classPath;
      }
    }
    if(!path.equals("")) {       //$NON-NLS-1$
      // eliminate duplicate jars
      jars.clear();
      String next = path;
      int i = path.indexOf(";"); //$NON-NLS-1$
      if(i==-1) {
        i = path.indexOf(":");   //$NON-NLS-1$
      }
      if(i!=-1) {
        next = path.substring(0, i);
        path = path.substring(i+1);
      } else {
        path = "";               //$NON-NLS-1$
      }
      // iterate thru path and add each unique jar to jars list
      while(next.length()>0) {
        if(!jars.contains(next)) {
          jars.add(next);
        }
        i = path.indexOf(";");   //$NON-NLS-1$
        if(i==-1) {
          i = path.indexOf(":"); //$NON-NLS-1$
        }
        if(i==-1) {
          next = path.trim();
          path = "";             //$NON-NLS-1$
        } else {
          next = path.substring(0, i).trim();
          path = path.substring(i+1).trim();
        }
      }
      // reconstruct clean path using semicolon delimiters
      Iterator<String> it = jars.iterator();
      while(it.hasNext()) {
        if(!path.equals("")) {   //$NON-NLS-1$
          path += ";";           //$NON-NLS-1$
        }
        path += it.next();
      }
    }
    // add ResourceLoader.launchJarName, if any, unless base directory path has been set
    if(OSPRuntime.getLaunchJarName()!=null
    		&& path.indexOf(OSPRuntime.getLaunchJarName())==-1
    		&& LaunchClassChooser.baseDirectoryPath==null) {
      if(!path.equals("")) { //$NON-NLS-1$
        path += ";";         //$NON-NLS-1$
      }
      path += OSPRuntime.getLaunchJarName();
    }
    return path;
  }

  /**
   * Sets the class path (relative jar or directory paths separated by colons or semicolons).
   *
   * @param jarNames the class path
   */
  public void setClassPath(String jarNames) {
    if((jarNames==null)||jarNames.equals("")) { //$NON-NLS-1$
      classPath = null;
      return;
    }
    // eliminate extra colons and semicolons
    while(jarNames.startsWith(":")||jarNames.startsWith(";")) { //$NON-NLS-1$ //$NON-NLS-2$
      jarNames = jarNames.substring(1);
    }
    while(jarNames.endsWith(":")||jarNames.endsWith(";")) { //$NON-NLS-1$ //$NON-NLS-2$
      jarNames = jarNames.substring(0, jarNames.length()-1);
    }
    String s = jarNames;
    int i = jarNames.indexOf(";;"); //$NON-NLS-1$
    if(i==-1) {
      i = jarNames.indexOf("::"); //$NON-NLS-1$
    }
    if(i==-1) {
      i = jarNames.indexOf(":;"); //$NON-NLS-1$
    }
    if(i==-1) {
      i = jarNames.indexOf(";:"); //$NON-NLS-1$
    }
    while(i>-1) {
      jarNames = jarNames.substring(0, i+1)+s.substring(i+2);
      s = jarNames;
      i = jarNames.indexOf(";;");   //$NON-NLS-1$
      if(i==-1) {
        i = jarNames.indexOf("::"); //$NON-NLS-1$
      }
      if(i==-1) {
        i = jarNames.indexOf(":;"); //$NON-NLS-1$
      }
      if(i==-1) {
        i = jarNames.indexOf(";:"); //$NON-NLS-1$
      }
    }
    classPath = jarNames;
  }

  /**
   * Returns the name of the class to launch
   * @return the class name
   */
  public String getLaunchClassName() { return this.launchClassName; }
  
  /**
   * Sets the launch class for this node.
   *
   * @param className the name of the class
   * @return true if the class was successfully loaded for the first time
   */
  public boolean setLaunchClass(String className) {
    if(className==null) {
      return false;
    }
    if((launchClassName==className)&&(launchClass!=null)) {
      return false;
    }
    OSPLog.finest(LaunchRes.getString("Log.Message.SetLaunchClass")+" "+className); //$NON-NLS-1$ //$NON-NLS-2$
    launchModelScroller = null;
    launchClassName = className;
    launchClass = LaunchClassChooser.getClass(getClassPath(), className);
    return launchClass!=null;
  }

  /**
   * Gets the launch class for this node.
   *
   * @return the launch class
   */
  public Class<?> getLaunchClass() {
    if((launchClass==null)&&(launchClassName!=null)&&!launchClassName.equals("")) { //$NON-NLS-1$
      setLaunchClass(launchClassName);
    }
    return launchClass;
  }

  /**
   * Gets the launch object. May be null.
   *
   * @return the launch object
   */
  public Object getLaunchObject() {
    if(launchObj!=null) {
      return launchObj;
    } else if(isRoot()) {
      return null;
    }
    LaunchNode node = (LaunchNode) getParent();
    return node.getLaunchObject();
  }

  /**
   * Sets the launch object.
   *
   * @param obj the launch object
   */
  public void setLaunchObject(Object obj) {
    launchObj = obj;
  }

  /**
   * Sets the specified display tab title and url path.
   * Specifying a tab number greater than the current tab count adds a new tab.
   * Setting a path to null removes the tab.
   *
   * @param n the tab number
   * @param title the tab title
   * @param path the path.
   * @param args model arguments. May be null.
   * @return the DisplayTab, or null if unsuccessful
   */
  public DisplayTab setDisplayTab(int n, String title, String path, String[] args) {
    if(n>=tabData.size()) {
      // add a new tab
      return addDisplayTab(title, path, args);
    } else if((path==null)||path.equals("")) {                           //$NON-NLS-1$
      // remove the tab
      return removeDisplayTab(n);
    } else {
      // change the tab
      DisplayTab tab = tabData.get(n);
      tab.title = title;
      tab.setPath(path);
      tab.setModelArgs(args);
      OSPLog.finest("tab "+n+" changed: [\""+title+"\", \""+path+"\"]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
      return tab;
    }
  }

  /**
   * Adds a display tab with the specified title and relative path.
   * No tab is added if path is null or "".
   *
   * @param title the tab title. May be null.
   * @param path the path.
   * @param args model arguments. May be null.
   * @return the newly created DisplayTab, if any
   */
  public DisplayTab addDisplayTab(String title, String path, String[] args) {
    if((path==null)||path.equals("")) {  //$NON-NLS-1$
      return null; 
    }
    // assemble DisplayTab and add to tabData
    DisplayTab tab = new DisplayTab(title, path);
    tab.setModelArgs(args);
    tabData.add(tab);
    OSPLog.finest("tab added: [\""+title+"\", \""+path+"\"]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    return tab;
  }

  /**
   * Inserts a display tab with the specified title and relative path.
   * No tab is inserted if path is null or "", or if the specified tab index
   * is invalid.
   *
   * @param n the insertion index
   * @param title the tab title. May be null.
   * @param path the path.
   * @param args model arguments. May be null.
   * @return the newly created DisplayTab, if any
   */
  public DisplayTab insertDisplayTab(int n, String title, String path, String[] args) {
    if((path==null)||path.equals("")||(n>=tabData.size())) { //$NON-NLS-1$
      return null; 
    }
    // assemble tab and insert into tabData
    DisplayTab tab = new DisplayTab(title, path);
    tab.setModelArgs(args);
    tabData.add(n, tab);
    OSPLog.finest("tab inserted: [\""+title+"\", \""+path+"\"]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    return tab;
  }

  /**
   * Removes a display tab.
   *
   * @param n the tab number
   * @return the DisplayTab removed, or null if none removed
   */
  public DisplayTab removeDisplayTab(int n) {
    DisplayTab tab = getDisplayTab(n);
    if(tab!=null) {
      tabData.remove(tab);
      OSPLog.finest("tab "+n+" removed: [\""+ //$NON-NLS-1$ //$NON-NLS-2$
        tab.title+"\", \""+tab.path+"\"]");   //$NON-NLS-1$ //$NON-NLS-2$
    }
    return tab;
  }

  /**
   * Gets a display tab.
   *
   * @param n the tab number
   * @return the DisplayTab, or null if none found
   */
  public DisplayTab getDisplayTab(int n) {
    if(n>=tabData.size()) {
      return null;
    }
    return tabData.get(n);
  }

  /**
   * Returns the number of display tabs for this node.
   */
  public int getDisplayTabCount() {
    return tabData.size();
  }

  /**
   * Gets the fileName.
   *
   * @return the fileName, assumed to be a relative path
   */
  public String getFileName() {
    return fileName;
  }

  /**
   * Gets the string path to this node, starting from the root.
   * This returns the names of the nodes in the path, separated by "/".
   *
   * @return the path
   */
  public String getPathString() {
    TreeNode[] nodes = getPath();
    LaunchNode next = (LaunchNode) nodes[0];
    StringBuffer path = new StringBuffer(next.name);
    for(int i = 1; i<nodes.length; i++) {
      next = (LaunchNode) nodes[i];
      path.append("/"+next.name); //$NON-NLS-1$
    }
    return path.toString();
  }

  /**
   * Sets the fileName. Accepts relative paths or
   * will convert absolute paths to relative.
   *
   * @param path the path to the file
   * @return the file name
   */
  public String setFileName(String path) {
    if(path==null) {
      fileName = null;
    } else {
      fileName = XML.getPathRelativeTo(path, Launcher.tabSetBasePath);
    }
    return fileName;
  }

  /**
   * Gets the parentSelfContained flag.
   *
   * @return true if parentSelfContained is true for this or an ancestor
   */
  public boolean isParentSelfContained() {
    if(parentSelfContained) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.isSelfContained();
  }

  /**
   * Gets the selfContained flag.
   *
   * @return true if selfContained is true for this or an ancestor
   */
  public boolean isSelfContained() {
    if(selfContained||isParentSelfContained()) {
      return true;
    }
    return false;
  }

  /**
   * Gets the previewing flag.
   *
   * @return true if previewing is true for this or an ancestor
   */
  public boolean isPreviewing() {
    if(previewing) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode node = (LaunchNode) getParent();
    return node.isPreviewing();
  }

  /**
   * Gets the saveHiddenNodes flag.
   *
   * @return true if saveHiddenNodes is true for this or an ancestor
   */
  public boolean isSavingHiddenNodes() {
    if(saveHiddenNodes) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode node = (LaunchNode) getParent();
    return node.isSavingHiddenNodes();
  }

  /**
   * Sets the selfContained flag.
   *
   * @param selfContained true if self contained
   */
  public void setSelfContained(boolean selfContained) {
    this.selfContained = selfContained;
  }

  /**
   * Gets the singleVM flag.
   *
   * @return true if singleVM is true for this or an ancestor
   */
  public boolean isSingleVM() {
    if(singleVM||org.opensourcephysics.display.OSPRuntime.isWebStart()) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return singleVMOff ? false : parent.isSingleVM();
  }

  /**
   * Sets the single VM flag.
   *
   * @param singleVM true if single vm
   */
  public void setSingleVM(boolean singleVM) {
    this.singleVM = singleVM;
  }

  /**
   * Gets the showLog value.
   *
   * @return true if showLog is true for this or an ancestor
   */
  public boolean isShowLog() {
    if(showLog) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.isShowLog();
  }

  /**
   * Sets the showLog flag.
   *
   * @param show true to show the OSPLog (single vm only)
   */
  public void setShowLog(boolean show) {
    showLog = show;
  }

  /**
   * Gets the clearLog value.
   *
   * @return true if clearLog is true for this or an ancestor
   */
  public boolean isClearLog() {
    if(clearLog) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.isClearLog();
  }

  /**
   * Sets the showLog flag.
   *
   * @param clear true to clear the OSPLog (single vm only)
   */
  public void setClearLog(boolean clear) {
    clearLog = clear;
  }

  /**
   * Gets the log level.
   *
   * @return the level
   */
  public Level getLogLevel() {
    if(isRoot()) {
      return logLevel;
    }
    LaunchNode parent = (LaunchNode) getParent();
    if(parent.isShowLog()) {
      Level parentLevel = parent.getLogLevel();
      if(parentLevel.intValue()<logLevel.intValue()) {
        return parentLevel;
      }
    }
    return logLevel;
  }

  /**
   * Sets the log level.
   *
   * @param level the level
   */
  public void setLogLevel(Level level) {
    if(level!=null) {
      logLevel = level;
    }
  }

  /**
   * Gets the singleApp value.
   *
   * @return true if singleApp is true for this or an ancestor
   */
  public boolean isSingleApp() {
    if(singleApp) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return singleAppOff ? false : parent.isSingleApp();
  }

  /**
   * Sets the singleApp flag.
   *
   * @param singleApp true to close other apps when launching new app (single vm only)
   */
  public void setSingleApp(boolean singleApp) {
    this.singleApp = singleApp;
  }

  /**
   * Sets the hiddenWhenRoot flag.
   *
   * @param hide true to hide node when at root
   */
  public void setHiddenWhenRoot(boolean hide) {
    hiddenWhenRoot = hide;
  }

  /**
   * Gets the buttonView value.
   *
   * @return true if buttonView is true for this or an ancestor
   */
  public boolean isButtonView() {
    if(buttonView) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.isButtonView();
  }

  /**
   * Sets the buttonView flag.
   *
   * @param buttonView true to display in buttonView
   */
  public void setButtonView(boolean buttonView) {
    LaunchNode root = (LaunchNode) getRoot();
    root.buttonView = buttonView;
  }

  /**
   * Gets the singleton value.
   *
   * @return true if singleApp is true for this or an ancestor
   */
  public boolean isSingleton() {
    if(singleton) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.isSingleton();
  }

  /**
   * Sets the singleton flag.
   *
   * @param singleton true to allow single instance when in separate vm
   */
  public void setSingleton(boolean singleton) {
    this.singleton = singleton;
  }

  /**
   * Gets the hiddenInLauncher value.
   *
   * @return true if hiddenInLauncher is true for this or an ancestor
   */
  public boolean isHiddenInLauncher() {
    if(hiddenInLauncher) {
      return true;
    } else if(isRoot()) {
      return false;
    }
    LaunchNode parent = (LaunchNode) getParent();
    return parent.isHiddenInLauncher();
  }

  /**
   * Sets the hiddenInLauncher flag.
   *
   * @param hide true to hide this node in Launcher
   */
  public void setHiddenInLauncher(boolean hide) {
    hiddenInLauncher = hide;
  }

  /**
   * Gets the resource, if any, for this node
   *
   * @return the resource
   */
  public Resource getResource() {
    if(fileName==null) {
      return null;
    }
    String path = XML.getResolvedPath(fileName, Launcher.tabSetBasePath);
    return ResourceLoader.getResource(path);
  }

  /**
   * Determines whether a resource exists for this node.
   *
   * @return true if a resource exists
   */
  public boolean exists() {
    return(getResource()!=null);
  }

  /**
   * Return an existing file with current file name and specified base.
   * May return null.
   *
   * @return existing file, or null
   */
  public File getFile() {
    if(exists()) {
      return getResource().getFile();
    }
    return null;
  }

  /**
   * Determines if this node matches another node.
   *
   * @param node the node to match
   * @return true if the nodes match
   */
  public boolean matches(LaunchNode node) {
    if(node==null) {
      return false;
    }
    boolean match = (showLog==node.showLog)&&(clearLog==node.clearLog)&&(singleton==node.singleton)&&(singleVM==node.singleVM)&&(hiddenWhenRoot==node.hiddenWhenRoot)&&name.equals(node.name)&&description.equals(node.description)&&args[0].equals(node.args[0])&&(((fileName==null)&&(node.fileName==null))||((fileName!=null)&&fileName.equals(node.fileName)))&&(((getLaunchClass()==null)&&(node.getLaunchClass()==null))||((getLaunchClass()!=null)&&getLaunchClass().equals(node.getLaunchClass())))&&(((classPath==null)&&(node.classPath==null))||((classPath!=null)&&classPath.equals(node.classPath)));
    return match;
  }

  /**
   * Gets a child node specified by fileName.
   *
   * @param childFileName the file name of the child
   * @return the first child found, or null
   */
  public LaunchNode getChildNode(String childFileName) {
    Enumeration<?> e = breadthFirstEnumeration();
    while(e.hasMoreElements()) {
      LaunchNode next = (LaunchNode) e.nextElement();
      if(childFileName.equals(next.fileName)) {
        return next;
      }
    }
    return null;
  }

  /**
   * Adds menu item to a JPopupMenu or JMenu.
   *
   * @param menu the menu
   */
  public void addMenuItemsTo(final JComponent menu) {
    Enumeration<?> e = children();
    while(e.hasMoreElements()) {
      LaunchNode child = (LaunchNode) e.nextElement();
      if(child.isLeaf()) {
        JMenuItem item = new JMenuItem(child.toString());
        menu.add(item);
        item.setToolTipText(child.tooltip);
        item.setActionCommand(child.getID());
        item.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            String id = e.getActionCommand();
            LaunchNode root = (LaunchNode) LaunchNode.this.getRoot();
            Enumeration<?> e2 = root.postorderEnumeration();
            while(e2.hasMoreElements()) {
              LaunchNode node = (LaunchNode) e2.nextElement();
              if(node.getID().equals(id)) {
                node.launch();
                break;
              }
            }
          }

        });
      } else {
        JMenu item = new JMenu(child.toString());
        menu.add(item);
        child.addMenuItemsTo(item);
      }
    }
  }

  /**
   * Adds an action to this node's actions collection.
   *
   * @param action the action to add
   */
  public void addTerminateAction(Action action) {
    actions.add(action);
    launchCount++;
  }

  /**
   * Removes an action from this node's actions collection.
   *
   * @param action the action to remove
   */
  public void removeTerminateAction(Action action) {
    actions.remove(action);
    launchCount = Math.max(0, --launchCount);
  }

  /**
   * Removes an action from this node's actions collection.
   *
   * @param action the action to remove
   */
  public void terminate(Action action) {
    if(actions.contains(action)) {
      removeTerminateAction(action);
      if(launchPanel!=null) {
        launchPanel.repaint();
      }
    }
  }

  /**
   * Terminates all apps launched by this node.
   */
  public void terminateAll() {
    for(Iterator<Process> it = processes.iterator(); it.hasNext(); ) {
      Process proc = it.next();
      proc.destroy();
    }
    for(Iterator<Frame> it = frames.iterator(); it.hasNext(); ) {
      Frame frame = it.next();
      WindowListener[] listeners = frame.getWindowListeners();
      for(int j = 0; j<listeners.length; j++) {
        if(listeners[j] instanceof Launcher.FrameCloser) {
          frame.removeWindowListener(listeners[j]);
        }
      }
      frame.dispose();
    }
    for(Iterator<Thread> it = threads.values().iterator(); it.hasNext(); ) {
      Thread thread = it.next();
      if(thread!=null) {
        thread.interrupt();
      }
    }
    Collection<Action> allActions = new HashSet<Action>(actions);
    for(Iterator<Action> it = allActions.iterator(); it.hasNext(); ) {
      Action action = it.next();
      if(action!=null) {
        action.actionPerformed(null);
      }
    }
    processes.clear();
    frames.clear();
    threads.clear();
    actions.clear();
    launchCount = 0;
  }

  /**
   * Gets the launchModelScroller. May return null.
   *
   * @return the scroller containing the modelPane from the launch class
   */
  protected JScrollPane getLaunchModelScroller() {
    if(launchModelScroller==null) {
      final JComponent content = Launcher.getModelPane(getLaunchClass(), getArgs());
      if(content==null) {
        return null;
      }
      JPanel panel = new JPanel(new OSPLayout()) {
        public Dimension getPreferredSize() {
          Dimension dim = content.getPreferredSize();
          dim.width += 8;
          dim.height += 8;
          return dim;
        }

      };
      panel.setBackground(java.awt.Color.white);
      panel.add(content, OSPLayout.CENTERED);
      launchModelScroller = new JScrollPane(panel);
    }
    return launchModelScroller;
  }

  /**
   * Returns the XML.ObjectLoader for this class.
   *
   * @return the object loader
   */
  public static XML.ObjectLoader getLoader() {
    return new Loader();
  }

  /**
   * A class to save and load LaunchNode data in an XMLControl.
   */
  private static class Loader extends XMLLoader {
    public void saveObject(XMLControl control, Object obj) {
      LaunchNode node = (LaunchNode) obj;
      node.setMinimumArgLength(1); // trim args if nec
      if(!node.name.equals("")) {            //$NON-NLS-1$
        control.setValue("name", node.name); //$NON-NLS-1$
      }
      if(!node.description.equals("")) {                   //$NON-NLS-1$
        control.setValue("description", node.description); //$NON-NLS-1$
      }
      if(!node.tooltip.equals("")) {               //$NON-NLS-1$
        control.setValue("tooltip", node.tooltip); //$NON-NLS-1$
      }
      if(!node.xsetName.equals("")) {                 //$NON-NLS-1$
        control.setValue("launchset", node.xsetName); //$NON-NLS-1$
      }
      control.setValue("display_tabs", node.getDisplayData()); //$NON-NLS-1$
      control.setValue("display_args", node.getDisplayArgs()); //$NON-NLS-1$
      if(node.getLaunchClass()!=null) {
        control.setValue("launch_class", node.getLaunchClass().getName()); //$NON-NLS-1$
      } else if(node.launchClassName!=null) {
        control.setValue("launch_class", node.launchClassName);            //$NON-NLS-1$
      }
      if(!node.args[0].equals("")||(node.args.length>1)) { //$NON-NLS-1$
        control.setValue("launch_args", node.args);        //$NON-NLS-1$
      }
      if((node.classPath!=null)&&!node.classPath.equals("")) { //$NON-NLS-1$
        control.setValue("classpath", node.classPath);         //$NON-NLS-1$
      }
      if(node.hiddenWhenRoot) {
        control.setValue("root_hidden", true); //$NON-NLS-1$
      }
      if(node.buttonView) {
        control.setValue("button_view", true); //$NON-NLS-1$
      }
      if(node.singleton) {
        control.setValue("singleton", true); //$NON-NLS-1$
      }
      if(node.singleVM) {
        control.setValue("single_vm", true); //$NON-NLS-1$
      }
      if(node.singleVMOff) {
        control.setValue("single_vm_off", true); //$NON-NLS-1$
      }
      if(node.showLog) {
        control.setValue("show_log", true); //$NON-NLS-1$
      }
      if(node.logLevel!=DEFAULT_LOG_LEVEL) {
        control.setValue("log_level", node.logLevel.getName()); //$NON-NLS-1$
      }
      if(node.clearLog) {
        control.setValue("clear_log", true); //$NON-NLS-1$
      }
      if(node.singleApp) {
        control.setValue("single_app", true); //$NON-NLS-1$
      }
      if(node.singleAppOff) {
        control.setValue("single_app_off", true); //$NON-NLS-1$
      }
      if(node.hiddenInLauncher) {
        control.setValue("hidden_in_launcher", true); //$NON-NLS-1$
      }
      if(!node.author.equals("")) {              //$NON-NLS-1$
        control.setValue("author", node.author); //$NON-NLS-1$
      }
      if(!node.keywords.equals("")) {                //$NON-NLS-1$
        control.setValue("keywords", node.keywords); //$NON-NLS-1$
      }
      if(!node.level.equals("")) {             //$NON-NLS-1$
        control.setValue("level", node.level); //$NON-NLS-1$
      }
      if(!node.languages.equals("")) {                 //$NON-NLS-1$
        control.setValue("languages", node.languages); //$NON-NLS-1$
      }
      if(!node.comment.equals("")) {               //$NON-NLS-1$
        control.setValue("comment", node.comment); //$NON-NLS-1$
      }
      if(!node.appletWidth.equals("")) {                    //$NON-NLS-1$
        control.setValue("applet_width", node.appletWidth); //$NON-NLS-1$
      }
      if(!node.appletHeight.equals("")) {                     //$NON-NLS-1$
        control.setValue("applet_height", node.appletHeight); //$NON-NLS-1$
      }
      //      Dimension dim = EjsTool.getEjsAppletDimension(node.getLaunchClass());
      //      if(dim!=null) {
      //        control.setValue("applet_width", dim.width); //$NON-NLS-1$
      //        control.setValue("applet_height", dim.height); //$NON-NLS-1$
      //      }
      if(node.children!=null) {
        // list the children and save the list
        ArrayList<Serializable> children = new ArrayList<Serializable>();
        Enumeration<?> e = node.children();
        boolean saveAll = node.isSavingHiddenNodes();
        while(e.hasMoreElements()) {
          LaunchNode child = (LaunchNode) e.nextElement();
          if(!saveAll&&child.isHiddenInLauncher()) {
            continue;
          }
          if(node.isPreviewing()) {
            children.add(child);
          } else if((child.fileName!=null)&&!node.isSelfContained()) {
            children.add(child.fileName);
          } else {
            child.fileName = null;
            child.setSelfContained(false);
            children.add(child);
          }
        }
        if(children.size()>0) {
          control.setValue("child_nodes", children); //$NON-NLS-1$
        }
      }
    }

    public Object createObject(XMLControl control) {
      String name = control.getString("name"); //$NON-NLS-1$
      if(name==null) {
        name = LaunchRes.getString("NewNode.Name"); //$NON-NLS-1$
      }
      return new LaunchNode(name);
    }

    public Object loadObject(XMLControl control, Object obj) {
      LaunchNode node = (LaunchNode) obj;
      String name = control.getString("name"); //$NON-NLS-1$
      if(name!=null) {
        node.name = name;
      }
      String description = control.getString("description"); //$NON-NLS-1$
      if(description!=null) {
        node.description = description;
      }
      String tooltip = control.getString("tooltip"); //$NON-NLS-1$
      if(tooltip!=null) {
        node.tooltip = tooltip;
      }
      String xsetName = control.getString("launchset"); //$NON-NLS-1$
      if(xsetName!=null) {
        node.xsetName = xsetName;
      }
      // read legacy xml files with "url" properties
      String url = control.getString("url"); //$NON-NLS-1$
      if(url!=null) {
        node.setDisplayData(new String[][] {
          {null, url}
        });
      } else {
        if(control.getPropertyNames().contains("html")) {                      //$NON-NLS-1$ pre-September 2009 files
          node.setDisplayData((String[][]) control.getObject("html"));         //$NON-NLS-1$
        } else {                                                               // post-September 2009 files
          node.setDisplayData((String[][]) control.getObject("display_tabs")); //$NON-NLS-1$
        }
        node.setDisplayArgs((String[][]) control.getObject("display_args"));   //$NON-NLS-1$
      }
      node.setClassPath(control.getString("classpath")); //$NON-NLS-1$
      String className = control.getString("launch_class"); //$NON-NLS-1$
      if(className!=null) {
        node.launchClassName = className;
        node.launchModelScroller = null;
      }
      String[] args = (String[]) control.getObject("launch_args"); //$NON-NLS-1$
      if(args!=null) {
        node.setArgs(args);
      }
      node.hiddenWhenRoot = control.getBoolean("root_hidden");          //$NON-NLS-1$
      node.buttonView = control.getBoolean("button_view");              //$NON-NLS-1$
      node.singleton = control.getBoolean("singleton");                 //$NON-NLS-1$
      node.singleVM = control.getBoolean("single_vm");                  //$NON-NLS-1$
      node.singleVMOff = control.getBoolean("single_vm_off");           //$NON-NLS-1$
      node.showLog = control.getBoolean("show_log");                    //$NON-NLS-1$
      node.clearLog = control.getBoolean("clear_log");                  //$NON-NLS-1$
      node.singleApp = control.getBoolean("single_app");                //$NON-NLS-1$
      node.singleAppOff = control.getBoolean("single_app_off");         //$NON-NLS-1$
      node.hiddenInLauncher = control.getBoolean("hidden_in_launcher"); //$NON-NLS-1$
      Level logLevel = OSPLog.parseLevel(control.getString("log_level")); //$NON-NLS-1$
      if(logLevel!=null) {
        node.logLevel = logLevel;
      }
      // meta items should not be set to null
      String author = control.getString("author"); //$NON-NLS-1$
      if(author!=null) {
        node.author = author;
      }
      String keywords = control.getString("keywords"); //$NON-NLS-1$
      if(keywords!=null) {
        node.keywords = keywords;
      }
      String level = control.getString("level"); //$NON-NLS-1$
      if(level!=null) {
        node.level = level;
      }
      String lang = control.getString("languages"); //$NON-NLS-1$
      if(lang!=null) {
        node.languages = lang;
      }
      String comment = control.getString("comment"); //$NON-NLS-1$
      if(comment!=null) {
        node.comment = comment;
      }
      String width = control.getString("applet_width"); //$NON-NLS-1$
      if(width!=null) {
        node.appletWidth = width;
      }
      String height = control.getString("applet_height"); //$NON-NLS-1$
      if(height!=null) {
        node.appletHeight = height;
      }
      name = control.getString("filename"); //$NON-NLS-1$
      if(name!=null) {
        node.setFileName(name);
      }
      Collection<?> children = (ArrayList<?>) control.getObject("child_nodes"); //$NON-NLS-1$
      if(children!=null) {
        node.removeAllChildren();
        Iterator<?> it = children.iterator();
        while(it.hasNext()) {
          Object next = it.next();
          if(next instanceof LaunchNode) {                                                                                                                                                            // child node
            // add the child node
            LaunchNode child = (LaunchNode) next;
            node.add(child);
            child.setLaunchClass(child.launchClassName);
          } else if(next instanceof String) {                                                                                                                                                         // file name
            String fileName = (String) next;
            String path = XML.getResolvedPath(fileName, Launcher.tabSetBasePath);
            // last path added is first path searched
            ResourceLoader.addSearchPath(Launcher.resourcesPath);
            ResourceLoader.addSearchPath(Launcher.tabSetBasePath);
            // open the file in an xml control
            XMLControlElement childControl = new XMLControlElement();
            String absolutePath = childControl.read(path);
            if(childControl.failedToRead()) {
              JOptionPane.showMessageDialog(null, LaunchRes.getString("Dialog.InvalidXML.Message")+" \""+fileName+"\"", LaunchRes.getString("Dialog.InvalidXML.Title"), JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
            }
            // check root to prevent circular references
            LaunchNode root = (LaunchNode) node.getRoot();
            if((root.getChildNode(fileName)!=null)||fileName.equals(root.fileName)) {
              continue;
            }
            Class<?> type = childControl.getObjectClass();
            if(LaunchNode.class.isAssignableFrom(type)) {
              // first add the child with just the fileName
              // this allows descendants to get the root
              LaunchNode child = new LaunchNode(LaunchRes.getString("NewNode.Name"));      //$NON-NLS-1$
              child.setFileName(fileName);
              OSPLog.finest(LaunchRes.getString("Log.Message.Loading")+": "+absolutePath); //$NON-NLS-1$ //$NON-NLS-2$
              node.add(child);
              // then load the child with data
              childControl.loadObject(child);
            }
          }
        }
      }
      return node;
    }

  }

  /**
   * Gets the display tab data.
   *
   * @return an array of String[2] {title, path}, one for each tab
   */
  private String[][] getDisplayData() {
    if(tabData.isEmpty()) {
      return null;
    }
    String[][] data = new String[tabData.size()][2];
    for(int i = 0; i<tabData.size(); i++) {
      DisplayTab tab = tabData.get(i);
      data[i] = new String[] {tab.title, tab.path};
    }
    return data;
  }

  /**
   * Sets the display tab data.
   *
   * @param data an array of String[2] {title, path}, one for each tab
   */
  private void setDisplayData(String[][] data) {
    if(data==null) {
      return;
    }
    tabData.clear();
    for(int i = 0; i<data.length; i++) {
      DisplayTab tab = new DisplayTab(data[i][0], data[i][1]);
      tabData.add(tab);
    }
  }

  /**
   * Gets the arguments for display tab models.
   *
   * @return an array of String[] arguments, one for each tab
   */
  private String[][] getDisplayArgs() {
    if(tabData.isEmpty()) {
      return null;
    }
    String[][] data = new String[tabData.size()][];
    for(int i = 0; i<tabData.size(); i++) {
      DisplayTab tab = tabData.get(i);
      String[] args = tab.getModelArgs();
      data[i] = (args.length==0) ? null : args;
    }
    return data;
  }

  /**
   * Sets the arguments for display tab models.
   *
   * @param data an array of String[] arguments, one for each tab
   */
  private void setDisplayArgs(String[][] data) {
    if(data==null) {
      return;
    }
    int len = Math.min(data.length, tabData.size());
    for(int i = 0; i<len; i++) {
      DisplayTab tab = tabData.get(i);
      tab.setModelArgs(data[i]);
    }
  }

  protected void setMinimumArgLength(int n) {
    n = Math.max(n, 1); // never shorter than 1 element
    if(n==args.length) {
      return;
    }
    if(n>args.length) {
      // increase the size of the args array
      String[] newArgs = new String[n];
      for(int i = 0; i<n; i++) {
        if(i<args.length) {
          newArgs[i] = args[i];
        } else {
          newArgs[i] = "";                                     //$NON-NLS-1$
        }
      }
      setArgs(newArgs);
    } else {
      while((args.length>n)&&args[args.length-1].equals("")) { //$NON-NLS-1$
        String[] newArgs = new String[args.length-1];
        for(int i = 0; i<newArgs.length; i++) {
          newArgs[i] = args[i];
        }
        setArgs(newArgs);
      }
    }
  }

  protected void removeThread(Runnable runner) {
    threads.remove(runner);
  }

  /**
   * A class to hold display tab data.
   */
  public class DisplayTab {
    String title;
    boolean hyperlinksEnabled = true;
    String path; // url path or model class name
    URL url;
    Class<?> modelClass;
    JComponent modelPane;
    JScrollPane modelScroller;
    String[] modelArgs = new String[0];

    /**
     * Constructor.
     *
     * @param title the tab title (may be null)
     * @param path the path
     */
    DisplayTab(String title, String path) {
      setTitle(title);
      setPath(path);
    }

    /**
     * Gets the path.
     *
     * @return the path
     */
    public String getPath() {
      return path;
    }

    /**
     * Sets the path.
     *
     * @param path the path
     */
    public void setPath(String path) {
      this.path = path;
      if(!setURL(path)) {
        setModelClass(path);
      }
    }

    /**
     * Gets the title.
     *
     * @return the title
     */
    public String getTitle() {
      return title;
    }

    /**
     * Sets the title.
     *
     * @param title the title
     */
    public void setTitle(String title) {
      this.title = title;
    }

    /**
     * Gets the URL. May return null.
     *
     * @return the URL
     */
    public URL getURL() {
      if((url==null)&&(modelClass==null)&&(path!=null)&&!"".equals(path)) { //$NON-NLS-1$
        setURL(path);
      }
      return url;
    }

    /**
     * Gets the model class. May return null.
     *
     * @return the model class
     */
    public Class<?> getModelClass() {
      if((url==null)&&(modelClass==null)&&(path!=null)&&!path.equals("")) { //$NON-NLS-1$
        setModelClass(path);
      }
      return modelClass;
    }

    /**
     * Gets the modelPane. May return null.
     *
     * @return the model pane
     */
    public JComponent getModelPane() {
      if((modelPane==null)&&(getModelClass()!=null)) {
        if(JComponent.class.isAssignableFrom(modelClass)) {
          try {
            modelPane = (JComponent) modelClass.newInstance();
          } catch(Exception ex) {}
        } else {
          modelPane = Launcher.getModelPane(modelClass, getModelArgs());
        }
      }
      return modelPane;
    }

    /**
     * Gets the model arguments. May return null.
     *
     * @return the model arguments
     */
    public String[] getModelArgs() {
      return modelArgs;
    }

    /**
     * Sets the model arguments.
     *
     * @param args the model arguments
     */
    public void setModelArgs(String[] args) {
      if(args!=null) {
        modelArgs = args;
        modelPane = null;
        modelScroller = null;
      }
    }

    /**
     * Gets the modelScroller. May return null.
     *
     * @return the model scroller
     */
    protected JScrollPane getModelScroller() {
      final JComponent content = getModelPane();
      if((modelScroller==null)&&(content!=null)) {
        JPanel panel = new JPanel(new OSPLayout()) {
          public Dimension getPreferredSize() {
            Dimension dim = content.getPreferredSize();
            dim.width += 8;
            dim.height += 8;
            return dim;
          }

        };
        panel.setBackground(java.awt.Color.white);
        panel.add(content, OSPLayout.CENTERED);
        modelScroller = new JScrollPane(panel);
      }
      return modelScroller;
    }

    protected void setMinimumModelArgLength(int n) {
      n = Math.max(n, 0);
      if(n==modelArgs.length) {
        return;
      }
      if(n>modelArgs.length) {
        // increase the size of the modelArgs array
        String[] newArgs = new String[n];
        for(int i = 0; i<n; i++) {
          if(i<modelArgs.length) {
            newArgs[i] = modelArgs[i];
          } else {
            newArgs[i] = null;
          }
        }
        setModelArgs(newArgs);
      } else {
        while((modelArgs.length>n)&&(modelArgs[modelArgs.length-1]==null)) {
          String[] newArgs = new String[modelArgs.length-1];
          for(int i = 0; i<newArgs.length; i++) {
            newArgs[i] = modelArgs[i];
          }
          setModelArgs(newArgs);
        }
      }
    }

    /**
     * Sets the URL.
     *
     * @param path the url path
     * @return true if a url resource was found
     */
    private boolean setURL(String path) {
      url = null;
      Resource res = ResourceLoader.getResource(path);
      if((res!=null)&&(res.getURL()!=null)) {
        url = res.getURL();
        try {
          InputStream in = url.openStream();
          in.close();
          OSPLog.finer(LaunchRes.getString("Log.Message.URL")+" "+url); //$NON-NLS-1$ //$NON-NLS-2$
        } catch(Exception ex) {
          url = null;
        }
      }
      return url!=null;
    }

    /**
     * Sets the model class. The model class must either descend
     * from JPanel or define a static getModelPane() method.
     *
     * @param className the name of the class
     * @return true if the class was successfully loaded for the first time
     */
    private boolean setModelClass(String className) {
      path = className;
      if(className==null) {
        return false;
      }
      if((modelClass!=null)&&className.equals(modelClass.getName())) { // no change      
        return false;
      }
      modelPane = null;
      modelScroller = null;
      modelClass = LaunchClassChooser.getModelClass(getClassPath(), className);
      return modelClass!=null;
    }

  }

  //___________________________ deprecated methods _________________________

  /**
   * Adds an HTML tab with the specified title and relative path.
   * @deprecated replaced by addDisplayTab
   */
  public DisplayTab addHTML(String title, String path) {
    return addDisplayTab(title, path, null);
  }

  /**
   * Inserts an HTML tab with the specified title and relative path.
   * @deprecated replaced by insertDisplayTab
   */
  public DisplayTab insertHTML(int n, String title, String path) {
    return insertDisplayTab(n, title, path, null);
  }

  /**
   * Removes an HTML tab.
   * @deprecated replaced by removeDisplayTab
   */
  public DisplayTab removeHTML(int n) {
    return removeDisplayTab(n);
  }

  /**
   * Gets an HTML tab.
   * @deprecated replaced by getDisplayTab
   */
  public DisplayTab getHTML(int n) {
    return getDisplayTab(n);
  }

  /**
   * Returns the number of html objects in the list.
   * @deprecated replaced by getDisplayTabCount
   */
  public int getHTMLCount() {
    return getDisplayTabCount();
  }

  /**
   * Sets the specified HTML tab title and url path.
   * @deprecated replaced by setDisplayTab
   */
  public DisplayTab setHTML(int n, String title, String path) {
    return setDisplayTab(n, title, path, null);
  }

}

/*
 * 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
 */
