package ca.ucalgary.seahawk.gui;

import java.awt.*;
import java.awt.event.*;
import java.awt.print.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.Semaphore;

import javax.swing.*;
import javax.swing.event.*;

import javax.xml.transform.stream.*;
import javax.xml.transform.*;
import javax.xml.parsers.*;

import org.w3c.dom.*;
import org.w3c.tidy.Tidy;

import org.biomoby.client.*;
import org.biomoby.client.util.SplashScreenStatus;
import org.biomoby.registry.meta.*;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;

import ca.ucalgary.seahawk.util.*;
import ca.ucalgary.seahawk.services.MobyClient;

/**
 * Main interface component: textually displays the data in a MOBY content XML document.
 *  For details on embedding in your own app, please see <a href="http://biomoby.open-bio.org/CVS_CONTENT/moby-live/Java/docs/Seahawk.html#component">the documentation</a>.
 *
 * @author Paul Gordon (gordonp@ucalgary.ca)
 */

public class MobyContentGUI extends JFrame implements ActionListener, ComponentListener, KeyListener, MobyRequestEventHandler, MouseListener{

    // Variables used to coordinate component finding with the unit test cases
    public final static String BACK_BUTTON_NAME = "MCGbackButton";
    public final static String FORWARD_BUTTON_NAME = "MCGforwardButton";
    public final static String FORWARD_BUTTON_MSG = "Go to next document in this tab's history";
    public final static String OPEN_BUTTON_NAME = "MCGopenButton";
    public final static String SAVE_BUTTON_NAME = "MCGsaveButton";
    public final static String FILTER_BUTTON_NAME = "MCGfilterButton";
    public final static String PRINT_BUTTON_NAME = "MCGprintButton";
    public final static String HELP_BUTTON_NAME = "MCGhelpButton";
    public final static String SETTINGS_BUTTON_NAME = "MCGsettingsButton";
    public final static String OPEN_OPTION_NAME = "MCGopenPopup";
    public final static String FILE_OPEN_OPTION_NAME = "MCGopenPopupFileOption";
    public final static String WEB_OPEN_OPTION_NAME = "MCGopenPopupWebOption";
    public final static String TABBED_PANE_NAME = "MCGtabbedPane";

    public final static String FILE_CHOOSER_OPEN_TITLE = "Load data from file";
    public final static String WEB_ADDR_DIALOG_TITLE = "Load Data From A Web Address (URL)";

    public final static String DEFAULT_STATUS_MSG = "Click hyperlink to explore MOBY Web services           ";
    public final static String RESOURCE_SYSTEM_PROPERTY = "seahawk.stylesheet";
    public final static String DEFAULT_STARTUP_PAGE_RESOURCE = "ca/ucalgary/seahawk/resources/startup.html";
    public final static String DEFAULT_XSLT_CONVERTER_URL = "ca/ucalgary/seahawk/resources/moby2HTML.xsl";
    public final static int MAX_TAB_NAME_LENGTH = 25;
    public final static String CLIPBOARD_TAB_TOOLTIP = "Right-click for clipboard options";
    public final static String TAB_TOOLTIP = "Right-click for tab options, or Ctrl-T for new tab";
    public final static String CLOSE_TAB_OPTION = "Close This Tab";
    public final static String CLOSE_OTHERS_OPTION = "Close Other Tabs";
    public final static String FILE_OPEN_OPTION = "Open File";
    public final static String WEB_OPEN_OPTION = "Open Web Page";

    public final static String SEAHAWK_NS_URI = "http://moby.ucalgary.ca/seahawk";
    public final static String SEAHAWK_NS_PREFIX = "seahawk";
    public final static String SEAHAWK_XPATH_ATTR = "xpath";

    /** if URL starts with this, it's a service input payload spec of the form "url\tproviderURI\tserviceName" */
    public static final String SERVICE_INPUT_MAGIC = "#!"; 

    private int lastClickX = 0;
    private int lastClickY = 0;
    private JTabbedPane tabbedPane;
    private JLabel status;
    private JPanel statusPanel;
    private JButton backwardButton;
    private JButton forwardButton;
    private JButton openButton;
    private JButton saveButton;
    private JButton filterButton;
    private JButton printButton;
    private JButton helpButton;
    private JButton settingsButton;
    private Transformer moby2HTMLConverter;  // XSLT engine
    private MobyServicesGUI servicesGUI;
    private DocumentBuilder docBuilder = null;
    private MobyContentClipboard clipboard;
    private MobyContentHelpPane helpPane;
    private DataFlowRecorder dataRecorder; // used for workflow export, etc.
    private SeahawkOptionsGUI settingsGUI;

    private boolean setup;
    private Map<Integer,MobyContentPane> request2tab;
    private int activeTabIndex = -1;
    
    private static int defaultCloseOperation = JFrame.EXIT_ON_CLOSE;
    private static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(MobyContentGUI.class);
    private static Acme.Serve.Serve servletContainer;

    /**
     * Constructor for a standalone visual interface.
     */
    public MobyContentGUI(MobyServicesGUI mgui){
	super();
	setTitle("Seahawk: MOBY Data Display");
	setPreferredSize(new Dimension(530, 400));
	servicesGUI = mgui;
	request2tab = new TreeMap<Integer,MobyContentPane>();
	setup = false;
	addComponentListener(this);

	// Setup, but don't show, the GUI
	statusPanel = new JPanel();
	ClassLoader cl = getClass().getClassLoader();
	if(cl == null){
	    cl = ClassLoader.getSystemClassLoader();
	}
	BoxLayout box = new BoxLayout(statusPanel, BoxLayout.LINE_AXIS);
	statusPanel.setLayout(box);

	Dimension buttonSize = new Dimension(20,20);
	backwardButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/back.gif")));
	backwardButton.setDisabledIcon(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/back_d.gif")));
	backwardButton.setToolTipText("Go to previous document in this tab's history");
	backwardButton.setMinimumSize(buttonSize);
	backwardButton.setPreferredSize(buttonSize);
	backwardButton.setMaximumSize(buttonSize);
	backwardButton.setEnabled(false);
	backwardButton.addActionListener(this);
	backwardButton.setName(BACK_BUTTON_NAME); // so test cases can find the component
	statusPanel.add(backwardButton);

	forwardButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/forward.gif")));
	forwardButton.setDisabledIcon(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/forward_d.gif")));
	forwardButton.setToolTipText(FORWARD_BUTTON_MSG);
	forwardButton.setMinimumSize(buttonSize);
	forwardButton.setPreferredSize(buttonSize);
	forwardButton.setMaximumSize(buttonSize);
	forwardButton.setEnabled(false);
	forwardButton.addActionListener(this);
	forwardButton.setName(FORWARD_BUTTON_NAME);
	statusPanel.add(forwardButton);

	filterButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/search.gif")));
	filterButton.setDisabledIcon(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/search_d.gif")));
	filterButton.setToolTipText("Search and filter the page contents");
	filterButton.setMinimumSize(buttonSize);
	filterButton.setPreferredSize(buttonSize);
	filterButton.setMaximumSize(buttonSize);
	filterButton.setEnabled(true);
	filterButton.addActionListener(this);
	filterButton.setName(FILTER_BUTTON_NAME);
	statusPanel.add(filterButton);

	openButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/bookmarks.gif")));
	openButton.setToolTipText("Open a document from file or the Web");
	openButton.setMinimumSize(buttonSize);
	openButton.setPreferredSize(buttonSize);
	openButton.setMaximumSize(buttonSize);
	openButton.setEnabled(true);
	openButton.addActionListener(this);
	openButton.addMouseListener(this);
	openButton.setName(OPEN_BUTTON_NAME);
	statusPanel.add(openButton);

	saveButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/save.gif")));
	saveButton.setDisabledIcon(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/save_d.gif")));
	saveButton.setToolTipText("Save this tab's document to a file");
	saveButton.setMinimumSize(buttonSize);
	saveButton.setPreferredSize(buttonSize);
	saveButton.setMaximumSize(buttonSize);
	saveButton.setEnabled(true);
	saveButton.addActionListener(this);
	saveButton.setName(SAVE_BUTTON_NAME);
	statusPanel.add(saveButton);

	printButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/print.gif")));
	printButton.setToolTipText("Print this tab's document as shown");
	printButton.setMinimumSize(buttonSize);
	printButton.setPreferredSize(buttonSize);
	printButton.setMaximumSize(buttonSize);
	printButton.setEnabled(true);
	printButton.addActionListener(this);
	printButton.setName(PRINT_BUTTON_NAME);
	statusPanel.add(printButton);

	helpButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/help.gif")));
	helpButton.setToolTipText("Show help on using this interface");
	helpButton.setMinimumSize(buttonSize);
	helpButton.setPreferredSize(buttonSize);
	helpButton.setMaximumSize(buttonSize);
	helpButton.setEnabled(true);
	helpButton.addActionListener(this);
	helpButton.setName(HELP_BUTTON_NAME);
	statusPanel.add(helpButton);

	settingsButton = new JButton(new ImageIcon(cl.getResource("ca/ucalgary/seahawk/resources/images/settings.gif")));
	settingsButton.setToolTipText("Show program options");
	settingsButton.setMinimumSize(buttonSize);
	settingsButton.setPreferredSize(buttonSize);
	settingsButton.setMaximumSize(buttonSize);
	settingsButton.setEnabled(true);
	settingsButton.addActionListener(this);
	settingsButton.setName(SETTINGS_BUTTON_NAME);
	statusPanel.add(settingsButton);

	statusPanel.add(Box.createRigidArea(new Dimension(5,0)));

	status = new JLabel(DEFAULT_STATUS_MSG, SwingConstants.LEFT);
	//JPanel labelPanel = new JPanel();
	//labelPanel.setLayout(new BorderLayout());
	status.setPreferredSize(new Dimension(380, buttonSize.height));
	//labelPanel.setPreferredSize(new Dimension(380, buttonSize.height));
	//labelPanel.add(status, BorderLayout.WEST); //use a label panel to have a fixed size for the status message
	//statusPanel.add(labelPanel);
	statusPanel.add(status);
	statusPanel.add(Box.createHorizontalGlue());
	getContentPane().add(statusPanel, java.awt.BorderLayout.SOUTH);
	setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);

	tabbedPane = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.WRAP_TAB_LAYOUT);
	tabbedPane.setName(TABBED_PANE_NAME);
	// See OptionsTabbedPaneUI class definiton at the end of this file (overrides right-click behaviour)
	tabbedPane.setUI(new OptionsTabbedPaneUI(this));
	tabbedPane.setSize(getContentSize());
	tabbedPane.setToolTipText("Hit <Control-T> to display a new tab");
	tabbedPane.addKeyListener(this);
	
	try{
	    dataRecorder = new DataFlowRecorder(servicesGUI.getMobyCentralImpl());
	} catch(Exception e){
	    e.printStackTrace();
	}

	clipboard = new MobyContentClipboard(this,
					     servicesGUI, 
					     tabbedPane,
					     dataRecorder,
					     status);
	getContentPane().add(tabbedPane, java.awt.BorderLayout.CENTER);
	tabbedPane.add(clipboard);
	clipboard.init();

	settingsGUI = new SeahawkOptionsGUI(this);
    }

    public Acme.Serve.Serve getServletContainer(){
	return servletContainer;
    }

    /**
     * A shared resource for proxying requests such as WSDL PBE wrapping
     */
    public void setServletContainer(Acme.Serve.Serve sc){
	servletContainer = sc;
    }

    public int processCGIResults(byte[] responseBody, String contentType) throws Exception{
	// Easy case: just like a Web Service
	if(contentType.indexOf("xml") != -1){
	    return processServiceResults(getDocumentBuilder().parse(new ByteArrayInputStream(responseBody)));
	}
	else if(contentType.indexOf("html") != -1){
	    // turn it into xhtml using jtidy, then apply the rules as per usual
	    return processServiceResults(convertHTMLToXHTML(responseBody));
	}
	// Maybe an image or something?  Could be recognized by a rule.
	else{
	    //todo: client.getMobyObjects(responseBody);
	}
	return 0;
    }

    public static Document convertHTMLToXHTML(byte[] responseBody){
	Tidy tidy = new Tidy();
	tidy.setXHTML(true);
	tidy.setShowWarnings(false);
	return tidy.parseDOM(new ByteArrayInputStream(responseBody), null);
    }

    /**
     * This method is called by the Programming-by-example (PBE) system when
     * we have the response from a service that's being semantically wrapped.
     * To help the user out with the wrapping, we'll see if the MOB rules
     * pick up any MOBY objects in the data.  Otherwise they'll have to do extra
     * steps to cast the returned data, etc..
     *
     * @return the number of Moby objects found in the data.
     */
    public int processServiceResults(Node responseDOM) throws Exception{

	MobyClient client = servicesGUI.getMobyClient();
	// recursively traverse the DOM and check if any of the text nodes matches text-based MOB rules
	Vector<MobyDataObject> objectsFound = getMobyObjects(responseDOM, client, "", "");

	// Sort the data into sublists based on using identical XPaths (name-based, not position based), 
	// encoded in each object's articleName.  This allows us to generalize the xpath -> data type 
	// mapping the user should pick.
	Map<String,Vector<MobyDataObject>> xpath2objs = new HashMap<String,Vector<MobyDataObject>>();
	for(MobyDataObject data: objectsFound){
	    String dataXPath = data.getName().split("#")[0];
	    if(!xpath2objs.containsKey(dataXPath)){
		xpath2objs.put(dataXPath, new Vector<MobyDataObject>());
	    }
	    xpath2objs.get(dataXPath).add(data);
	}
	MobyContentInstance mci = new MobyContentInstance();
	for(String xPath: xpath2objs.keySet()){
	    MobyDataJob part = new MobyDataJob();
	    int i = 1;
	    MobyDataObjectSet set = new MobyDataObjectSet("all");
	    for(MobyDataObject obj: xpath2objs.get(xPath)){ 
		set.add(obj);
	    }
	    part.put("all", set);
	    mci.put("of the form "+xPath, part);
	}
	
	// Insert an instruction message for the user in the comments TODO
	mci.setServiceNotes("<b>NOTE:</b> To create from this example a " +
			    "<i><b>new Moby service</b></i> that can be called again later, " +
			    "1) click any link in the detected data below and " +
			    "select \""+MobyContentPane.SERVICE_CREATION_MSG+"\" from the " +
			    "popup menu, or 2) click a link in the parsed Web browser results.");

	// Now show the data in the Seahawk service-wrapping tab
	// Delete temp file when program exits.
	File resultFile = null;
	try{
	    resultFile = File.createTempFile(MobyContentPane.WRAPPING_RESULTFILE_PREFIX, ".xml");
	}
	catch(Exception e){
	    logger.error("Cannot create temp file for Moby objects found in service results:" + e);
	    return 0;
	}
	resultFile.deleteOnExit();

	FileOutputStream fos = new FileOutputStream(resultFile);
	MobyDataUtils.toXMLDocument(fos, mci);
	fos.close();

	for(int i = 0; i < tabbedPane.getTabCount(); i++){
	    MobyContentPane pane = (MobyContentPane) tabbedPane.getComponentAt(i);
	    if(pane.isWrappingService()){
		pane.gotoURL(resultFile.toURI().toURL(), false); //false = don't put in history
		tabbedPane.setSelectedIndex(i);
		break;
	    }
	}

	return objectsFound.size();
    }

    // xPointer is a bit of a misnomer, it's actualy a unique identifying xPath to give NS context for the xPath var's evaluation
    private Vector<MobyDataObject> getMobyObjects(Node node, MobyClient client, String xPath, String xPointer) throws Exception{
	Vector<MobyDataObject> results = new Vector<MobyDataObject>();

	// get XPath-based answers
	for(Map.Entry<String,MobyDataObject[]> generatedObjects: client.getMobyObjectsURNMap(node).entrySet()){
	    for(MobyDataObject xmlObject: generatedObjects.getValue()){
		xmlObject.setName(xPath+"#"+xPointer+"#"+generatedObjects.getKey());
		results.add(xmlObject);
	    }
	}

	// While we traverse the nodes looking for data, add an attr with the xpath of the element, 
	// as it may be useful for the PBE system later...
	NodeList childNodes = node.getChildNodes();
	int elementCount = 0;
	for(int i = 0; i < childNodes.getLength(); i++){
	    Node childNode = childNodes.item(i);
	    if(childNode instanceof Element){
		elementCount++;
		String newXPath = xPath+"/"+childNode.getNodeName();
		String newXPointer = xPointer+"/"+elementCount+"";
		((Element) childNode).setAttributeNS(SEAHAWK_NS_URI, SEAHAWK_NS_PREFIX+":"+SEAHAWK_XPATH_ATTR, 
						     newXPath+"#"+newXPointer);
		results.addAll(getMobyObjects(childNode, client, newXPath, newXPointer));
	    }
	    else if(childNode instanceof Attr){
		// Map<ruleURN,mobyObjectsGenerated>
		for(Map.Entry<String,MobyDataObject[]> generatedObjects: 
			client.getMobyObjectsURNMap(((Attr) childNode).getValue()).entrySet()){
		    for(MobyDataObject textObject: generatedObjects.getValue()){
			// generatedObjects.getKey() is the generating rule's URN
			textObject.setName(xPath+"/@"+childNode.getNodeName()+"#"+xPointer+"#"+generatedObjects.getKey());
			results.add(textObject);
		    }
		}
	    }
	    else if(childNode instanceof CharacterData){
		if(childNode instanceof Comment){
		    continue;
		}
		for(Map.Entry<String,MobyDataObject[]> generatedObjects: 
			client.getMobyObjectsURNMap(((CharacterData) childNode).getData()).entrySet()){
		    for(MobyDataObject textObject: generatedObjects.getValue()){
			textObject.setName(xPath+"/text()#"+xPointer+"#"+generatedObjects.getKey());
			results.add(textObject);
		    }
		}
	    }
	}

	return results;
    }

    /**
     * Explicitly stop the servlet container, if running, that is used for WSDL service wrapping.
     */
    public void stopServletContainer() throws IOException{
	servletContainer.notifyStop();
    }

    public boolean allTabsVisible(){
	if(tabbedPane.getTabLayoutPolicy() == JTabbedPane.WRAP_TAB_LAYOUT){
	    return true;
	}
	for(int i = 0; i < tabbedPane.getTabCount(); i++){
	    if(tabbedPane.getBoundsAt(i) == null){
		return false;  //bounds null if not showing on screen
	    }
	}
	return true;
    }

    public JPanel getToolbar(){
	return statusPanel;
    }

    public Transformer getTransformer(){
	if(!setup){
	    setup();
	}
	return moby2HTMLConverter;
    }


    public DocumentBuilder getDocumentBuilder(){
	if(!setup){
	    setup();
	}
	return docBuilder;
    }
    
    /**
     * Implemented to provide paste functionality (control-v or the paste button on a Sun keyboard),
     * since the editor panes are not editable and therefore by default do not respond to
     * paste events.
     */
    public void keyPressed(KeyEvent e){
	// Ctrl-T for new tab, like regular Web browsers
	if(e.getKeyCode() == KeyEvent.VK_T && e.isControlDown()){
	    MobyContentPane tab = createTab("New Tab");
	    tabbedPane.setSelectedComponent(tab);
	    tab.getDisplay().setText("Use the file/globe icon at the bottom of this window to load data," +
				     "or drag'n'drop/paste data from your desktop or Web browser.");
	}
    }

    public void keyReleased(KeyEvent e){}
    public void keyTyped(KeyEvent e){}

    /**
     * This method sets the layout, basic GUI elements,
     * initializes conditions, and so on so it'll be ready to go when gotoURL is called 
     * to load a MOBY document for the first time.
     */
    protected void setup(){

	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);	
	try{
	    docBuilder = dbf.newDocumentBuilder();
	}
	catch(ParserConfigurationException pce){
	    logger.error("Error: Could not find an XML parser, will not be able to query " +
			       "using complex MOBY objects: " + pce);
	}

	// Now setup the XSLT to transform MOBY XML into HTML for presentation
	// (only needs to be done once since we always use the same stylesheet)
	TransformerFactory factory = TransformerFactory.newInstance();
	ClassLoader cl = getClass().getClassLoader();
	if(cl == null){
	    cl = ClassLoader.getSystemClassLoader();
	}
        String stylesheetResource = System.getProperty(RESOURCE_SYSTEM_PROPERTY);
        if(stylesheetResource == null){
          stylesheetResource = DEFAULT_XSLT_CONVERTER_URL;
        }
	StreamSource stylesheet = new StreamSource(cl.getResource(stylesheetResource).toString());
	try{
	    moby2HTMLConverter = factory.newTransformer(stylesheet);
	}
	catch(TransformerConfigurationException tce){
	    MobyContentPane tab = createTab("Initialization Error");
	    tab.failed("Initialization Error");
	    tab.getDisplay().setText("Cannot continue: could not create XML transformer (either stylesheet has an error, or no JAXT implementation is available\n");
      	    logger.error("Cannot continue: could not create XML transformer: " + tce);
	    tce.printStackTrace();
	}

	pack();
	setVisible(true);
	//tabbedPane.add(clipboard);
	int clipboardIndex = tabbedPane.indexOfComponent(clipboard);
	if(clipboardIndex != -1){
	    tabbedPane.setToolTipTextAt(clipboardIndex, CLIPBOARD_TAB_TOOLTIP);
	}
	clipboard.setPreferredSize(getContentSize());
	clipboard.init();
	setup = true;
    }
    
    /**
     * Called by MOBYRequest when the service request is being sent.  We
     * can create the GUI and wait message here.
     */
    public void start(MobyRequestEvent requestEvent){
	if(!setup){
	    setup();
	}
	MobyContentPane tab = createTab("New Request");
	request2tab.put(new Integer(requestEvent.getID()), tab);
	tab.start(requestEvent);
    }

    protected MobyContentPane createTab(String title){
	MobyContentPane tab = new MobyContentPane(this, 
						  servicesGUI, 
						  tabbedPane,
						  dataRecorder,
						  status);
	setVisible(true);
	tab.setPreferredSize(getContentSize());
	tabbedPane.addTab(title, tab);
	int tabIndex = tabbedPane.indexOfComponent(tab);
	if(tabIndex != -1){
	    tabbedPane.setToolTipTextAt(tabIndex, TAB_TOOLTIP);
	}
	return tab;
    }
	

    /** Calculates the approximate space available for the content on a pane (i.e. frame size - tab size - status bar)*/
    public Dimension getContentSize(){
	Dimension insetdims = getContentPane().getSize();
	insetdims.width -= 6;

	int tabHeight = 0;
	if(tabbedPane != null){
	    tabbedPane.setSize(insetdims);
	    for(int i = 0; i < tabbedPane.getTabCount(); i++){
		Rectangle rect = tabbedPane.getBoundsAt(i);
		if(rect != null && rect.getSize().height > tabHeight){
		    tabHeight = rect.getSize().height;
		}
	    }
	}
	int statusHeight = 0;
	if(status != null){
	    statusHeight = status.getSize().height;
	}
	insetdims.height -= statusHeight+tabHeight+16;
	return insetdims;
    }

    public void setStatus(String msg){
	status.setText(msg);
	status.setToolTipText(msg);
    }

    public void componentHidden(ComponentEvent e){}
    public void componentMoved(ComponentEvent e){}
    public void componentShown(ComponentEvent e){}

    /** Makes the panels of the tabbed pane have a preferred size to fit within the frame */
    public void componentResized(ComponentEvent e){
	if(tabbedPane != null){
	    Dimension insetdims = getContentSize();
	    tabbedPane.setSize(insetdims);
	    for(int i = 0; i < tabbedPane.getTabCount(); i++){
		((MobyContentPane) tabbedPane.getComponentAt(i)).setPreferredSize(insetdims);
	    }
	}
    }

    public void stop(MobyRequest request, int requestID){
	MobyContentPane tab = request2tab.get(new Integer(requestID));
	if(tab == null){
	    logger.warn("Ignoring request to stop response monitoring, because the tab for request " +
			       requestID + " no longer exists");
	}
	else{
	    tab.stop(request, requestID);
	}
    }

    // Called by MobyRequest when the response is available
    public void processEvent(MobyRequestEvent mre){
	setVisible(true);
	MobyContentPane tab = request2tab.get(new Integer(mre.getID()));
	if(tab == null){
	    logger.warn("Ignoring request to display response, because the tab for request " +
			       mre.getID() + " no longer exists");
	}
	else{
	    tab.processEvent(mre);
	}
    }

    /**
     * Allow others to set the forward navigation button message.  If set the null, the default tooltip is used.
     */
    public void setForwardButtonToolTip(String msg){
	forwardButton.setToolTipText(msg == null ? FORWARD_BUTTON_MSG : msg);
    }

    public void updateHistoryButtons(){
	MobyContentPane pane = (MobyContentPane) tabbedPane.getSelectedComponent();
	if(pane != null){
	    backwardButton.setEnabled(pane.canGoBack());
	    forwardButton.setEnabled(pane.canGoForward());
	    filterButton.setEnabled(pane.canFilter());
	    saveButton.setEnabled(!pane.hasFailure());
	}
    }

    /**
     * A pane whose history list has changed should call this method
     * to ensure the forward and backward buttons are properly set
     * to reflect the change.
     */
    public void updateHistory(MobyContentPane pane){
	if(pane == tabbedPane.getSelectedComponent()){
	    updateHistoryButtons();
	}
    }

    public void goBackward(){
	if(tabbedPane.getTabCount() != 0){
	    ((MobyContentPane) tabbedPane.getSelectedComponent()).goBackward();
	}
    }

    public void goForward(){
	if(tabbedPane.getTabCount() != 0){
	    ((MobyContentPane) tabbedPane.getSelectedComponent()).goForward();
	}
    }

    /**
     * Reports whetehr the current tab (if any is showing) is displaying a failure, such as
     * network or protocol failure while requesting a service.
     */
    public boolean isShowingFailure(){
	MobyContentPane pane = getCurrentPane();
	return pane != null && pane.hasFailure();
    }

    /**
     * Returns the URL of the document that is the source of the current tab's display
     */
    public URL getCurrentURL(){
	if(tabbedPane.getTabCount() == 0){
	    return null;
	}
	return ((MobyContentPane) tabbedPane.getSelectedComponent()).getCurrentURL();
    }

    /**
     * Returns the current tab's display component, mostly for testing purposes.
     */
    public MobyContentPane getCurrentPane(){
	if(tabbedPane.getTabCount() == 0){
	    return null;
	}
	return (MobyContentPane) tabbedPane.getSelectedComponent();
    }

    public void printCurrentPane(){
	PrinterJob job = PrinterJob.getPrinterJob();
	job.setPrintable((Printable) tabbedPane.getSelectedComponent());
	if (job.printDialog()){ /* Displays the standard system print dialog */	   
	    try{
		job.print();
	    }
	    catch (Exception ex){
		JOptionPane.showMessageDialog(null, "Cannot print the MOBY document: "+ex, 
					      "Error printing", JOptionPane.ERROR_MESSAGE);
	    }
	}
    }

    public CentralImpl getMobyCentralImpl(){
	if(servicesGUI != null){
	    return servicesGUI.getMobyCentralImpl();
	}
	else{
	    return null;
	}
    }

    public void saveCurrentPane(){
	MobySaveDialog.exportDocument((MobyContentPane) tabbedPane.getSelectedComponent());
    }

    /**
     * @return the tab index for the help tab
     */
    public int showHelpTab(){
	if(helpPane == null){
	    helpPane = new MobyContentHelpPane(this,
					       servicesGUI, 
					       tabbedPane,
					       dataRecorder,
					       status);
	}
	
	// Not a tab in the display yet
	if(tabbedPane.indexOfComponent(helpPane) == -1){
	    tabbedPane.add(helpPane);
	    helpPane.setPreferredSize(getContentSize());
	    helpPane.init();
	}
	tabbedPane.setSelectedComponent(helpPane);
	return tabbedPane.getSelectedIndex();
    }
    
    /**
     * Displays the choice to the user to open a file or URL.
     * Choice callback will call either showFileDialog or showWebDialog.
     */
    public void openDocument(){
	JPopupMenu fileOrWeb = new JPopupMenu();
	fileOrWeb.setName(OPEN_OPTION_NAME);

	JMenuItem fileOption = new JMenuItem(FILE_OPEN_OPTION);
	fileOption.setName(FILE_OPEN_OPTION_NAME);
	fileOption.addActionListener(this);
	fileOrWeb.add(fileOption);

	JMenuItem webOption = new JMenuItem(WEB_OPEN_OPTION);
	webOption.setName(WEB_OPEN_OPTION_NAME);
	webOption.addActionListener(this);
	fileOrWeb.add(webOption);

	//java.awt.Point panelLoc = statusPanel.getLocation();
	fileOrWeb.show(openButton, lastClickX, lastClickY);	
    }

    public void showWebDialog(boolean useDefaultHandler){
	URL url = null;
	String urlString = "";
	while(url == null){
	    urlString = (String) JOptionPane.showInputDialog(this,
						    "Please enter a valid Web address:\n",
						    WEB_ADDR_DIALOG_TITLE, 
						    JOptionPane.PLAIN_MESSAGE,
						    (javax.swing.Icon) null,
						    (Object []) null,//not a combo box
						    urlString); // default value
	    try{
		url = new URL(urlString);
	    }
	    catch(MalformedURLException murle){
		Object[] options = {"Try again", "Cancel"};
		int response = JOptionPane.showOptionDialog(this,
							    "Sorry, the provided address couldn't be used.\n"+
							    "(note only http, file and ftp addresses are allowed)",
							    "Invalid Web Address (URL)",
							    JOptionPane.OK_CANCEL_OPTION,
							    JOptionPane.QUESTION_MESSAGE,
							    null,
							    options,
							    options[0]);
		if(response == 1){  //Cancel
		    break;
		}
		// else we go to next loop iteration with new default url value in dialog
	    }
	}

	// Got a good URL?
	if(url != null){
	    loadPaneFromURL(url, useDefaultHandler);
	}
    }

    public MobyService findService(String providerURI, String serviceName) throws Exception{
	CentralImpl central = getMobyCentralImpl();
	MobyService[] services = central.findService(new MobyService(serviceName, providerURI)); 
	if(services == null || services.length == 0){
	    throw new Exception("Could not find the new service ("+providerURI+","+serviceName+
				") in the registry.");
	}
	else if(services.length > 1){
	    throw new Exception("More than one service matching ("+providerURI+","+serviceName+
				") was found in the registry, something is seriously wrong!");
	}
	return services[0];
    }

    public MobyContentInstance callService(MobyService service, MobyContentInstance inEnvelope) throws Exception{
	MobyRequest request = new MobyRequest(getMobyCentralImpl());
	request.setService(service);
	request.setInput(inEnvelope);
	return request.invokeService();
    }

    /**
     * Called after a service has been created by the wrapping system, with the intention
     * that the new service should be called and included in the browsing session.
     */
    public void serviceWrapped(String providerURI, String serviceName, MobyDataJob sampleData) throws Exception{
	MobyContentInstance inEnvelope = new MobyContentInstance();
	inEnvelope.put("demo", sampleData);
	MobyService service = findService(providerURI, serviceName);
	MobyContentInstance result = callService(service, inEnvelope);

	// If any of the input data had peers, allow the user to invoke the newly created service
	// on all of its peers.
	String peerOptionsHTML = "";
	for(MobyPrimaryData templateData: service.getPrimaryInputs()){
	    MobyDataInstance data = sampleData.get(templateData.getName());
	    if(!(data instanceof MobyPrimaryData)){
		// wierdness
		logger.warn("Service parameter "+templateData.getName()+" is a primary parameter, but " +
			    "the sample data provided was of class "+data.getClass().getName());
		continue;
	    }
	    if(data.getUserData() == null){
		// untraceable for some reason.  The UserData should have been passed along with
		// the MobyDataInstance from MobyContentPane.getDraggedData(), all the way through 
		// PBE recorder...
		logger.warn("Sample MobyDataInstance for new service did not have provenance " +
			    "data in UserData as expected, skipping peer checking for parameter "+
			    templateData.getName());
		continue;
	    }
	    // MobyPayloadCreator contains the code to recreate the peer set of the sample data
	    // as it was the moment the data was dragged into the service wrapping demo.
	    MobyPayloadCreator srcDataInContext = new MobyPayloadCreator(null, (String) data.getUserData());
	    MobyContentInstance peerJobs = srcDataInContext.createPayload(templateData.getName());
	    if(peerJobs != null && peerJobs.size() > 1){
		String type = ((MobyPrimaryData) data).getDataType().getName();
		if(type.equals(MobyTags.MOBYOBJECT) && ((MobyPrimaryData) data).getNamespaces().length > 0){
		    type = ((MobyPrimaryData) data).getNamespaces()[0].getName();
		}
		// Create a moby payload input file that the user can use to send all peers 
		// with the same parameters as the demo
		URL tempFile = DataUtils.createServiceInputFileForPeers(peerJobs, sampleData);
		peerOptionsHTML += ("<b><a href=\""+SERVICE_INPUT_MAGIC+tempFile.toString()+"\t"+
				    providerURI+"\t"+serviceName+
				    "\">Click here to run <i>" + serviceName + 
				    "</i> for the demo "+type+"'s peers (" + peerJobs.size() +
				    " total)</i></a></b><br/>").replaceAll("<", "&lt;");
	    }
	}
	if(peerOptionsHTML.length() != 0){
	    result.setServiceNotes(peerOptionsHTML);
	}

	URL resultURL = DataUtils.saveOutputData(result, service, //for tracking purposes
						 inEnvelope, getMobyCentralImpl().getRegistryEndpoint());  

	loadWrappingPanel(resultURL);
    }

    private void loadWrappingPanel(URL resultURL){
	for(int i = 0; i < tabbedPane.getTabCount(); i++){
	    MobyContentPane pane = (MobyContentPane) tabbedPane.getComponentAt(i);
	    if(pane.isWrappingService()){
		tabbedPane.setSelectedIndex(i);
		loadPaneFromURL(resultURL, true); //true = put in history
		break;
	    }
	}	
    }

   /**
     * Load a Seahawk browser tab with the data held in the given MobyContentInstance.
     * Note that this methods take a snapshot of the data when it is passed in.  Subsequent
     * changes to the MobyContentInstance will not be reflected in the Seahawk display.
     *
     * @throws Exception if a temporary file cannot be written or read
     */
    public void loadPaneFromObject(MobyContentInstance mci, boolean useDefaultHandler) throws Exception{
	File savedXMLFile = File.createTempFile("seahawk", ".xml");
	savedXMLFile.deleteOnExit();
	
	// Serialize, save to a file, then reload using existing load-from-URL function
	MobyDataUtils.toXMLDocument(new FileOutputStream(savedXMLFile), mci);

	// Load it back from file (data snapshot)
	loadPaneFromURL(savedXMLFile.toURI().toURL(), useDefaultHandler);
    }


    public void loadPaneFromURL(URL u, boolean useDefaultHandler){
	logger.debug("Should load URL " + u);
	MobyContentPane currentPane = (MobyContentPane) tabbedPane.getSelectedComponent();
	// The "default handler" is for us to create a new tab and show the data in it 
	if(useDefaultHandler || currentPane == null || currentPane.getDefaultHandler() == null){
	    currentPane = createTab("File Loading");
	}
	currentPane.setWaitScreen();

	currentPane.gotoURL(u, true);
	if(u != null){
	    currentPane.succeeded(urlToTitle(u));
	}
	tabbedPane.setSelectedComponent(currentPane);
    }

    protected String urlToTitle(URL u){
	String shortName = u.toString();
	if(shortName.length() > MAX_TAB_NAME_LENGTH){
	    shortName = u.getProtocol();
	    String host = u.getHost();
	    if(host != null && shortName.length()+host.length() <= MAX_TAB_NAME_LENGTH+3){
		shortName = shortName+u.getHost();
	    }
	    else{
		shortName = shortName+"...";
	    }
	    String path = u.getPath();
	    if(path == null){
	    }
	    else if(shortName.length()+path.length() <= MAX_TAB_NAME_LENGTH+3){
		shortName = shortName+path;
	    }
	    else{
		int allowableLen = MAX_TAB_NAME_LENGTH-shortName.length();
		if(allowableLen > path.length()){
		    allowableLen = path.length();
		}
		shortName = shortName+"..."+path.substring(path.length()-allowableLen);
	    }
	}
	return shortName;
    }

    // Filters are per page, not common to the whole app
    private void setFilterVisible(boolean visible){
	MobyContentPane pane = getCurrentPane();
	if(pane != null){
	    try{
		pane.setFilterVisible(visible);
	    } catch(Exception e){
		logger.warn("Could not set search/filter visibility to " + visible + ": "+e.getMessage(), e);
	    }
	}
    }

    /** Called when a tab option (close) is selected */
    public void actionPerformed(ActionEvent e) {
	Object source = e.getSource();
	if(source == printButton){
	    printCurrentPane();
	    return;
	}
	else if(source == saveButton){
	    saveCurrentPane();
	    return;
	}
	else if(source == openButton){
	    openDocument();
	    return;
	}
	else if(source == backwardButton){
	    goBackward();
	    return;
	}
	else if(source == forwardButton){
	    goForward();
	    return;
	}
	else if(source == helpButton){
	    showHelpTab();
	    return;
	}
	else if(source == settingsButton){
	    settingsGUI.setVisible(true);
	    return;
	}
	else if(source == filterButton){
	    setFilterVisible(true);
	    return;
	}

	String cmd = e.getActionCommand();
	if(FILE_OPEN_OPTION.equals(cmd)){
	    MobySaveDialog.showFileOpenDialog(this, 
					      (e.getModifiers() & MobyServicesGUI.USE_DEFAULT_HANDLER_MASK) != 0);
	}
	else if(WEB_OPEN_OPTION.equals(cmd)){
	    showWebDialog((e.getModifiers() & MobyServicesGUI.USE_DEFAULT_HANDLER_MASK) != 0);
	}
	else if(CLOSE_TAB_OPTION.equals(cmd)){
	    if(activeTabIndex == -1 || activeTabIndex >= tabbedPane.getTabCount()){
		logger.warn("Cannot process close tab event: tab " + activeTabIndex +" no longer exists");
		return;
	    }
	    if(tabbedPane.getComponentAt(activeTabIndex) instanceof MobyContentClipboard){
		// Don't close clipboard, if somehow we were asked...
	    }
	    else{
		tabbedPane.remove(activeTabIndex);
	    }
	}
	else if(MobyContentClipboard.CLEAR_CLIPBOARD_OPTION.equals(cmd)){
	    clipboard.clearData();
	}
	else if(CLOSE_OTHERS_OPTION.equals(cmd)){
	    if(activeTabIndex == -1 || activeTabIndex >= tabbedPane.getTabCount()){
		logger.warn("Cannot process close other tabs event: tab " + activeTabIndex + " no longer exists");
		return;
	    }
	    Component keeper = tabbedPane.getComponentAt(activeTabIndex);
	    for(int i = 0; i < tabbedPane.getTabCount(); i++){
		Component comp = tabbedPane.getComponentAt(i);
		if(comp != keeper && comp != clipboard){
		    tabbedPane.remove(comp);
		    i--;
		}
	    }
	}
	else{
	    logger.warn(getClass().getName() + " ignoring unrecognized action command: " + cmd);
	    return;
	}
    }
    public void mouseClicked(MouseEvent e){}

    public void mouseEntered(MouseEvent e){}
    
    public void mouseExited(MouseEvent e){}
        public void mousePressed(MouseEvent e){
	lastClickX = e.getX();
	lastClickY = e.getY();
    }

    public void mouseReleased(MouseEvent e){
    }

    public int getActiveTab(){
	return activeTabIndex;
    }

    public void setActiveTab(int index){
	activeTabIndex = index;
    }

    public boolean removeFromClipboard(MobyDataInstance itemToDelete){
	return clipboard != null && clipboard.removeCollectionData(itemToDelete);
    }

    /**
     * Adds the given data to the Seahawk clipboard object collection.
     * 
     * @param itemToAdd if null, method simply returns the current clipboard contents
     *
     * @return the contents of the main clipboard query after the addition, or null if the clipboard does not exist for some reason
     */
    public MobyDataObjectSet addToClipboard(MobyDataInstance itemToAdd){
	if(clipboard == null){
	    return null;
	}
	if(itemToAdd != null){
	    clipboard.addCollectionData(itemToAdd);
	}
	return clipboard.getCollection();
    }

    /**
     * Removes any existing data from the clipboard.
     */
    public void clearClipboard(){
	if(clipboard != null){
	    clipboard.clearData();
	}
    }

    class OptionsTabbedPaneUI extends javax.swing.plaf.basic.BasicTabbedPaneUI{
	MobyContentGUI parent;
	
        public OptionsTabbedPaneUI(MobyContentGUI gui){
	    parent = gui;
	}

	// Replace default mouse listener with one that has special behaviour for right click 
	protected MouseListener createMouseListener(){
	    return new OptionsMouseHandler(parent);
	}

	protected void setVisibleComponent(Component component){
	    super.setVisibleComponent(component);
	    // Keep the history buttons in sync
	    if(component instanceof MobyContentPane){
		updateHistoryButtons();
	    }
	}

	class OptionsMouseHandler extends javax.swing.plaf.basic.BasicTabbedPaneUI.MouseHandler{
	    MobyContentGUI gui;
	    
	    public OptionsMouseHandler(MobyContentGUI g){
	        gui = g;
	    }
	    
	    public void mousePressed(MouseEvent e){	
		if(e.getButton() == MouseEvent.BUTTON3){
		    // Right-click, show option of closing tabs instead of 
		    // default behaviour (which selected the tab as active)
		    int index = tabbedPane.indexAtLocation(e.getX(), e.getY());
		    if(index == -1){
			return;  // not on a tab
		    }
		    parent.setActiveTab(index);
		    
		    JPopupMenu tabOptions = new JPopupMenu();
		    
		    JMenuItem item = null;
		    // Cannot close the clipboard
		    if(tabbedPane.getComponentAt(index) == clipboard){
			item = new JMenuItem(MobyContentClipboard.CLEAR_CLIPBOARD_OPTION);
			item.setActionCommand(MobyContentClipboard.CLEAR_CLIPBOARD_OPTION);
			item.addActionListener(gui);
			tabOptions.add(item);
		    }
		    else if(tabbedPane.getTabCount() > 1){
			item = new JMenuItem(MobyContentGUI.CLOSE_TAB_OPTION);
			item.setActionCommand(MobyContentGUI.CLOSE_TAB_OPTION);
			item.addActionListener(gui);
			tabOptions.add(item);
		    }

		    if(tabbedPane.getComponentAt(index) == clipboard && tabbedPane.getTabCount() > 1 ||
		       tabbedPane.getTabCount() > 2 ||
		       tabbedPane.indexOfComponent(clipboard) == -1 && tabbedPane.getTabCount() >= 2){
			item = new JMenuItem(MobyContentGUI.CLOSE_OTHERS_OPTION);
			item.setActionCommand(MobyContentGUI.CLOSE_OTHERS_OPTION);
			item.addActionListener(gui);
			tabOptions.add(item);
		    }

		    tabOptions.show(gui, e.getX(), e.getY());
		}
		else{
		    // Otherwise do whatever this handler normally does
		    super.mousePressed(e);
		}
	    }
	}  //end option handler class definition
    } //end option tabbed pane class definition

    /**
     * Value to be passed to application JFrame
     */
    public static void setDefaultAppCloseOperation(int code){
	defaultCloseOperation = code;
    }

    private static void renderSplashFrame(Graphics2D g){
	g.setComposite(AlphaComposite.Clear);
        g.fillRect(140,140,220,40);
        g.setPaintMode();
        g.setColor(Color.BLACK);
        g.drawString(SplashScreenStatus.getStatus(), 140, 150);
    }

    public static void main(String[] argv){
	final SplashScreen splash = SplashScreen.getSplashScreen();
	final Semaphore killSplash = new Semaphore(0);
	final Graphics2D g = splash == null ? null : splash.createGraphics();
        if (g == null || splash == null) {
            logger.warn("Graphics object for splash screen is null");
        }
	else{
	    new Thread(){ 
		public void run(){
		    for(;;) {
			renderSplashFrame(g);
			splash.update();
			try {
			    Thread.sleep(100);
			}
			catch(InterruptedException e) {
			}
			if(killSplash.tryAcquire()){
			    splash.close();
			    return;
			}
		    }
		}}.start();
	}

	SplashScreenStatus.setStatus("Setting Java properties");
	// This code is simply used to avoid the ClassLoader from searching for non-existent properties files,
	// etc. by forcing resource loading for the only supported locale, _en
	java.util.Locale.setDefault(java.util.Locale.ENGLISH);

	// Unless overridden on the command line, use the cached calls moby central impl
 	if(System.getProperty(CentralImpl.CENTRAL_IMPL_RESOURCE_NAME) == null){
 	    System.setProperty(CentralImpl.CENTRAL_IMPL_RESOURCE_NAME,
 			       "org.biomoby.client.CentralCachedCallsImpl");
 	}

	// Unless overridden on the command line, use xalan
 	if(System.getProperty("javax.xml.transform.TransformerFactory") == null){
 	    System.setProperty("javax.xml.transform.TransformerFactory",
 			       "org.apache.xalan.processor.TransformerFactoryImpl");
 	}

	// Setting this explicitly prevents repeated calls to the META-INF file encoding the same 
 	if(System.getProperty("org.apache.xerces.xni.parser.XMLParserConfiguration") == null){
 	   System.setProperty("org.apache.xerces.xni.parser.XMLParserConfiguration",
 			       "org.apache.xerces.parsers.StandardParserConfiguration");
 	}

	// Tries to ensure that the ontology cache from CentralImpl is put in a directory
	// unique to the username.  If not, could have problem with other users already using 
	// the cache dir without sharing permissions
	String oldTmpDir = System.getProperty("java.io.tmpdir");
	if(oldTmpDir.indexOf(System.getProperty("user.name")) == -1){
	    System.setProperty("java.io.tmpdir", oldTmpDir+File.separator+System.getProperty("user.name"));
	}

	//includes socket-less client
// 	System.setProperty("axis.ClientConfigFile",
// 			   "ca/ucalgary/services/util/client-config.wsdd");  

	// First, restore any user preferences (uses default settings file location)
	SplashScreenStatus.setStatus("Restoring user preferences");
	try{
	    SeahawkOptions.restoreSettings();
	} catch(Exception e){
	    e.printStackTrace();
	    System.err.println("Using default settings, could not restore Seahawk settings from file " +
			       SeahawkOptions.getDefaultsFile()+": "+ e);
	}
	// Asynchronously load up ontology data so it's ready when the user needs it.
	SplashScreenStatus.setStatus("Loading interface");
	MobyContentGUI gui = MobyUtils.getMobyContentGUI(new JLabel());
	gui.setDefaultCloseOperation(defaultCloseOperation);

	gui.pack();
	gui.setVisible(true);

	killSplash.release();
	if(argv.length != 0 && argv[0] != null && argv[0].length() != 0){
	    try{
		gui.loadPaneFromURL(new URL(argv[0]), true);
	    }
	    catch(Exception e){
		logger.error("Could not load "+argv[0]+" - loading help file instead");
		e.printStackTrace();
		gui.showHelpTab();
	    }
	}
	else{
	    URL startURL = gui.getClass().getClassLoader().getResource(DEFAULT_STARTUP_PAGE_RESOURCE);
	    if(startURL == null){
		logger.error("Could not load "+DEFAULT_STARTUP_PAGE_RESOURCE+" - loading help file instead");
		gui.showHelpTab();
	    }
	    else{
		gui.loadPaneFromURL(startURL, true);
	    }
	}	
    }

    protected static void cacheOntologies(){
	final Registry preferredRegistry = SeahawkOptions.getRegistry();

	// Remove stale cache files before doing anything else
	final long allowedAgeMillis = (long) SeahawkOptions.getCacheExpiry()*60*60*1000; // hours to milliseconds
	RegistryCache.deleteExpiredCacheFiles(allowedAgeMillis);

	// TODO: deal with CentralImpl.getDefaultCentral()

	// Redirect the registry endpoints to local cache files if available
	// preferredRegistry and/or dataDefURL may be null, if not cached, but that's okay to pass around
	// The following command will cache all data type definitions for this session
	new Thread(){
	    public void run(){
		try{
		    MobyServiceType.loadServiceTypes(RegistryCache.cacheRegistryOntology(preferredRegistry, 
											 Central.SERVICE_TYPES_RESOURCE_NAME,
											 allowedAgeMillis).toURI().toURL(),
						     preferredRegistry);
		} catch(Exception e){
		    e.printStackTrace();
		}
	    }}.start();
    }

    /**
     * Method to be called when an applet is being decommissioned.
     */
    public static void destroy(){
	ca.ucalgary.seahawk.util.MobyUtils.destroyMobyGUI();
    }

    public JLabel getStatusComponent() {
	return status;
    }
}
