package ca.ucalgary.seahawk.gui;

// For external links
import ca.ucalgary.seahawk.services.DaggooClient;
import ca.ucalgary.seahawk.util.*;
import ca.ucalgary.services.CGIServlet;
import ca.ucalgary.services.SoapServlet;
import ca.ucalgary.services.WrappingServlet;
import ca.ucalgary.services.util.PBERecorder;

import org.biomoby.client.MobyRequest;
import org.biomoby.client.MobyRequestEvent;
import org.biomoby.client.MobyRequestEventHandler;
import org.biomoby.registry.meta.Registry;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;
import org.biomoby.shared.parser.ServiceException;

import org.w3c.dom.*;
import org.xml.sax.InputSource;
import javax.servlet.http.HttpServlet;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import javax.xml.xpath.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.AttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.*;
import java.awt.print.*;
import java.awt.event.*;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
import java.util.regex.*;

/**
 * Text area contained in a Seahawk GUI tab.  Displays HTML, RTF, text etc. using a JEditorPane.
 * Also provides link handling (including service options popup) and drag 'n' drop capabilities.
 */

public class MobyContentPane extends JPanel implements Printable, CaretListener, HyperlinkListener, MouseListener, MouseMotionListener, MobyRequestEventHandler, KeyListener, ChangeListener, ClipboardOwner, ActionListener{
    public static final String MOBY_SERVICE_POPUP_NAME = "seahawkServicePopup";
    public static final String WAITING_TAB_ICON_RESOURCE = "ca/ucalgary/seahawk/resources/images/hourglass.gif";
    public static final String FAILED_TAB_ICON_RESOURCE = "ca/ucalgary/seahawk/resources/images/failed.gif";
    public static final String LOADED_TAB_ICON_RESOURCE = "ca/ucalgary/seahawk/resources/images/document.gif";
    // An undocumented "feature" of Java temp file creation is that assigned prefixes are turned to lower case,
    // therefore you want to make the prefix lower case if you're going to check for this prefix's existence in a file name
    // anywhere in your code.
    public static final String WRAPPING_RESULTFILE_PREFIX = "wrapping_results_parsed";
    public static final String SERVICE_CREATION_MSG = "Create a service returning this datatype";
    private static final int TAB_ICON_SPACER = 2;

    private static final String SEARCH_VISIBLE = "Search visible";
    private static final String SEARCH_HIDDEN = "Search hidden";

    private static ImageIcon hourglassIcon; 
    private static ImageIcon failedIcon; 
    private static ImageIcon loadedIcon; 
    private static ImageIcon closeIcon; 

    /** The name of the param in the stylesheet that will hold the Moby doc URL for XPointer usage */
    public final static String XSL_DOC_SOURCE_PARAM = "sourceURL";

    private JScrollPane scrollPane;
    protected JTabbedPane tabbedPane;
    private JLabel status;
    protected PrintableJEditorPane editorPane;  // does the HTML display
    protected FilterSearchWidget filterSearchWidget;  // provides functionality for filtering the page view 
    protected JPanel searchPanel;  // provides functionality for filtering the page view 
    protected MobyServicesGUI servicesGUI;
    protected MobyContentGUI contentGUI;
    private Vector<URL> history;
    private HashMap<URL,String> historyTabLabels;
    private HashMap<URL,FilterSearch> filterHistory;
    private HashMap<URL,FilterSearch> deletedFilters;
    private int historyIndex = -1;
    private int lastClickX = 1;
    private int lastClickY = 1;
    private boolean hasFailed = false;
    private boolean isContentsXML = false;
    private DataFlowRecorder dataRecorder;
    private List<String> xPtrsReferencedInNextService;
    private Map<String,String> filteredData; //Map<xptrOfFilteredData,>
    private boolean filterChanged;
    private Document currentDoc; //the doc DOM being actively filtered
    private MutableNodeList currentSelectedData = null;
    private StringBuffer currentSelectionXPath = null;
    private List<String> filterableNodes = null;  // List<xptr>
    private Map<String,Boolean> jobXPtrs = null;  // Map<xptr,has a HAS member>
    private List<AttributeSet> origStyles = null;
    private boolean firstDocRendering = true;

    // Next two items used for service wrapping
    private JMenuItem createServicePopupItem;
    private MobyDataInstance itemToReturn;

    // Text selection members
    private int dot;
    private int mark;
    private int oldMark;
    private int oldDot;
    private boolean dragging = false;
    private boolean isWrapping = false; // are we wrapping a service using PBE at the moment?
    private boolean wrappingEnabled = true;
    private String selectedTextData;
    private boolean overHyperlink = false;
    private URL lastHyperlinkDragged = null;
    private URL lastHyperlinkHovered = null;
    
    private static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(MobyContentPane.class);
    private static SoapServlet soapServlet;
    private static CGIServlet cgiServlet;

    // Randomize the port used for the wrapping servlet: avoids problems 
    // when multiple people are wrapping on the same machine
    private final static int portNum = 8254+((int) (1000*Math.random()));

    private final static String cgiServletPath = "/CGIServlet";
    private final static String wsServletPath = "/SOAPServlet";
    private static Acme.Serve.Serve servletContainer;
    private static Map<String,MobyContentGUI> id2GuiMap;

    public MobyContentPane(MobyContentGUI cGUI, MobyServicesGUI sGUI, JTabbedPane parentComponent, DataFlowRecorder recorder, JLabel statusBar){
	tabbedPane = parentComponent;
	status = statusBar;
	contentGUI = cGUI;
	servicesGUI = sGUI;
	dataRecorder = recorder;

	// The next line is what provides the drag 'n' drop capability
	addKeyListener(this);
	setTransferHandler(new FileAndTextTransferHandler(cGUI));

	tabbedPane.addChangeListener(this);

	// The following is a non-user editable pane for showing HTML
	editorPane = new PrintableJEditorPane();
	editorPane.addKeyListener(this);
	editorPane.setTransferHandler(getTransferHandler());
	//editorPane.setPreferredSize(parentComponent.getPreferredSize());
	editorPane.setEditable(false);
	editorPane.addMouseListener(this);
	editorPane.addMouseMotionListener(this);
	editorPane.addHyperlinkListener(this);
	editorPane.addCaretListener(this);
	scrollPane = new JScrollPane(editorPane);

	setLayout(new BorderLayout());
	add(scrollPane, BorderLayout.CENTER);
	searchPanel = new JPanel();
	try{
	    filterSearchWidget = new FilterSearchWidget(this);
	} catch(Exception e){
	    logger.warn("Could not create FilterSearchWidget: " + e.getMessage(), e);
	}
	//searchPanel.add(filterSearchWidget);  by default not showing
	add(searchPanel, BorderLayout.SOUTH);

	createServicePopupItem = new JMenuItem(SERVICE_CREATION_MSG);
	createServicePopupItem.addActionListener(this);

	history = new Vector<URL>();
	historyTabLabels = new HashMap<URL,String>();
	filterHistory = new HashMap<URL,FilterSearch>();
	deletedFilters = new HashMap<URL,FilterSearch>();
	xPtrsReferencedInNextService = new Vector<String>();
        currentSelectedData = new MutableNodeList();
	filteredData = new HashMap<String,String>();
	jobXPtrs = new HashMap<String,Boolean>();

	ClassLoader cl = getClass().getClassLoader();
	if(cl == null){
	    cl = ClassLoader.getSystemClassLoader();
	}
	if(hourglassIcon == null){
	    URL u = cl.getResource(WAITING_TAB_ICON_RESOURCE);
	    if(u == null){
		logger.warn("Could not find icon resource " + WAITING_TAB_ICON_RESOURCE);
	    }
	    else{
		hourglassIcon = new ImageIcon(u);
	    }
	}
	if(failedIcon == null){
	    URL u = cl.getResource(FAILED_TAB_ICON_RESOURCE);
	    if(u == null){
		logger.warn("Could not find icon resource " + FAILED_TAB_ICON_RESOURCE);
	    }
	    else{
		failedIcon = new ImageIcon(u);
	    }
	}
	if(loadedIcon == null){
	    URL u = cl.getResource(LOADED_TAB_ICON_RESOURCE);
	    if(u == null){
		logger.warn("Could not find icon resource " + LOADED_TAB_ICON_RESOURCE);
	    }
	    else{
		loadedIcon = new ImageIcon(u);
	    }
	}

	id2GuiMap = new HashMap<String,MobyContentGUI>();	    
    }

    public JTabbedPane getTabbedPaneParent(){
	return tabbedPane;
    }

    public DataFlowRecorder getDataFlowRecorder(){
	return dataRecorder;
    }

    public void stateChanged(ChangeEvent ce){
	int tabIndex = tabbedPane.indexOfComponent(this);
	Icon currentIcon = tabIndex < 0 ? null : tabbedPane.getIconAt(tabIndex);
	if(tabIndex == tabbedPane.getSelectedIndex() && 
	   tabbedPane.getTabCount() > 1){
	    try{
		if(currentIcon == null){
		    tabbedPane.setIconAt(tabIndex, new CloseTabIcon());
		    return;
		}
		// If there's a real (non-close-tab) single icon, add the close tab icon
		if(!(currentIcon instanceof CloseTabIcon)){
		    try{tabbedPane.setIconAt(tabIndex, new CloseTabIcon(currentIcon, TAB_ICON_SPACER));}
		    catch(Exception e){e.printStackTrace();}
		    tabbedPane.setDisabledIconAt(tabIndex, currentIcon);
		}
	    }
	    catch(Exception e){
		e.printStackTrace();
	    }

	} else{
	    if(currentIcon != null && currentIcon instanceof CloseTabIcon){
		tabbedPane.setIconAt(tabIndex, ((CloseTabIcon) currentIcon).getSecondIcon());
		tabbedPane.setDisabledIconAt(tabIndex, ((CloseTabIcon) currentIcon).getSecondIcon());
	    }
	}
    }

    public void actionPerformed(ActionEvent e){
	if(e.getSource() == createServicePopupItem){
	    if(itemToReturn != null){
		//todo: fix so cgi servlet and soap servlet can coexist nicely...need to track which we're using at the moment
		createService(cgiServlet != null ? cgiServletPath : wsServletPath);
	    }
	}
	else{
	    logger.warn("Content pane: ignoring unrecognized source of event " + e);
	}
    }

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

    // servletPath is either the CGI or SOAP servlet
    private void createService(String servletPath){
	// item name, set by MobyContentGUI's processResults(), is namedXPath#numericXPtr
	String[] locSpec = itemToReturn.getName().split("#");
	String xPath = locSpec[0];
	if(locSpec.length == 1){
	    logger.warn("Could not find a '#' in object name " + itemToReturn.getName());
	    return;
	}
	String xPtr = locSpec[1];

	// remove the last part of the xPath, which is actually the value, not the selection
	String valXPath = xPath.substring(xPath.lastIndexOf("/")+1);
	//xPath = xPath.substring(0, xPath.lastIndexOf("/"));

	//todo: MOBY_OUTPUT_NS_PARAM MOBY_OUTPUT_TYPE_PARAM
	String transformationParam = "";
	if(locSpec.length == 3){
	    transformationParam = "&"+PBERecorder.OUTPUT_RULE_URI_PARAM+"="+locSpec[2];	    
	}

	// we assume the wrapping form has already been launched
	// so we'll get the cookie and continue the process at the xpath selection stage
	URL formURL = null;
	try{
	    formURL = new URL("http://localhost:"+portNum+servletPath+"?"+
			      PBERecorder.CONTEXT_XPTR_PARAM+"="+xPtr+"&"+
			      PBERecorder.RETURN_XPATH_PARAM+"="+xPath+"&"+
			      PBERecorder.VALUE_XPATH_PARAM+"="+valXPath+
			      transformationParam);
	} catch (java.net.MalformedURLException murle){
	    logger.error("Could not create URL for wrapping form: " + murle);	    
	    return;
	}
	launchInWebBrowser(formURL);
    }

    public void setPreferredSize(Dimension dims){
	super.setPreferredSize(dims);
	scrollPane.setPreferredSize(new Dimension(dims.width, dims.height-searchPanel.getPreferredSize().height));
    }

    public boolean hasXMLSource(){
	return isContentsXML && historyIndex != -1;
    }

    public String getXMLSource() throws IOException{
	if(!hasXMLSource()){
	    return null;
	}
	return HTMLUtils.getURLContents(history.elementAt(historyIndex));	    
    }

    public String getHTMLSource(){
	return editorPane.getText();
    }

    public void setWrappingEnabled(boolean b){
	wrappingEnabled = b;
    }

    public boolean getWrappingEnabled(){
	return wrappingEnabled;
    }

    private boolean shouldWrapHTMLForm(String htmlText){
	if(!wrappingEnabled || 
	   htmlText.indexOf("<form") == -1 &&
	   (htmlText.indexOf("<FORM") == -1)){
	    return false;
	}
	else{
	    //maybe do some fancy checks at some point of data content 
	    //proportion / view vs. query purpose guessing?
	    return true;
	}
    }

    /** 
     * Launch a servlet create and instrument the CGI forms on the given Web page, 
     * and that'll capture user events while filling in the form to call the CGI.  
     * This'll allow a Moby service to be created for later calling.
     */
    private void loadWebForm(URL u){
	tabbedPane.setSelectedComponent(this);
	editorPane.setText("Please wait a moment while I study the Web page...");

	//true == allow viewing of URL contents instead of wrapping as option
	DaggooClient.Action action = DaggooClient.acceptAnExistingWrapper(u, this, servicesGUI, true);
	if(action != DaggooClient.Action.WRAP){
	    // call to the selected service is done by DaggooClient in this case
	    return;
	}

	// Launch the wrapping servlet if not launched yet
	editorPane.setText("Launching CGI service wrapper interface...check your Web browser window");

	if(cgiServlet == null){
	    cgiServlet = new CGIServlet();
	}
	initServletContainer(cgiServlet, cgiServletPath, u);
    }

    /** 
     * Launch a servlet create a CGI form from a WSDL document, and that'll capture user events 
     * while filling in the form to call a web service.  This'll allow a Moby service to be created
     * for later calling.
     */
    private void loadWSDL(URL u){
	tabbedPane.setSelectedComponent(this);
	editorPane.setText("Please wait a moment while I study the document...");

	if(DaggooClient.acceptAnExistingWrapper(u, this, servicesGUI)){
	    // call to the selected service is done by DaggooClient if returning true
	    return;
	}

	// Launch the wrapping servlet if not launched yet
	editorPane.setText("Launching Web Service wrapper interface...check your Web browser window");
	
	if(soapServlet == null){
	    soapServlet = new SoapServlet();
	}
	initServletContainer(soapServlet, wsServletPath, u);
    }

    private void initServletContainer(WrappingServlet servlet, String servletPath, URL u){ 
	// Launch the servlet
	if(contentGUI.getServletContainer() == null){

	    final Acme.Serve.Serve srv = new Acme.Serve.Serve();
	    java.util.Properties properties = new java.util.Properties();
	    properties.put("port", portNum);
	    srv.arguments = properties;
	    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
		    public void run() {
			try {
			    srv.notifyStop();
			}catch(Exception e) {
			    // May throw NullPointerException if server was already killed,
			    // or maybe an IOException otherwise
			    logger.warn("Trouble shutting down wrapping servlet on port " 
					+ portNum + ": " + e);
			}
			srv.destroyAllServlets();
		    }
		}));
	    new Thread(){public void run(){srv.serve();}}.start();
	    
	    PBERecorder recorder = new PBERecorder();
	    recorder.setGUIMap(id2GuiMap);
	    servlet.setRecorder(recorder);
	    // So we can stop the container later, assign the final var to a class scope one.
	    contentGUI.setServletContainer(srv); 
	    srv.addServlet(servletPath, servlet); // our deployment
	}
	else if(contentGUI.getServletContainer().getServlet(servletPath) == null){
	    contentGUI.getServletContainer().addServlet(servletPath, servlet); 
	}

	// uniq id and GUI link up allow unification of browser events and the Sehawk GUI for PBE
	String uniqID = ""+Math.random();
	id2GuiMap.put(uniqID, contentGUI);

	URL formURL = null;
	try{
	    formURL = new URL("http://localhost:"+portNum+servletPath+"?"+
			      WrappingServlet.SRC_PARAM+"="+u+"&"+
			      WrappingServlet.ID_PARAM+"="+uniqID);
	} catch (java.net.MalformedURLException murle){
	    logger.error("Could not create URL for wrapping form: " + murle);	    
	    return;
	}
	
	launchInWebBrowser(formURL);
    }

    private void launchInWebBrowser(URL u){
	// Launch the browser with the form the servlet hosts
	try{
	    Desktop desktop = Desktop.getDesktop();
	    if(!desktop.isSupported(Desktop.Action.BROWSE)) {
		editorPane.setText("Your Java does not support external browser launch.  " +
				   "Please manually launch a browser and go to the URL " +
				   u);
	    }
	    desktop.browse(new java.net.URI(u.toURI().toString().replace("+", "%2B")));
	} catch (java.net.URISyntaxException urise){
	    logger.error("Could not create URI from URL ("+u+"): " + urise);
	} catch (java.io.IOException ioe){
	    logger.error("Could not launch browser for form (" + u + "): " + ioe);
	    /*	    org.jdesktop.jdic.desktop.Desktop.browse(u); */
	} catch (Exception e){
	    logger.error("Could not launch browser for form (" + u + "): " + e);
	    editorPane.setText("Your Java does not support external browser launch.  " +
			       "Please manually launch a browser and go to the URL " +
			       u);
	}
    }

    /**
     * Tells whether the tab is in the middle of wrapping a Web Service at the moment.
     */
    public boolean isWrappingService(){
	return isWrapping;
    }

    /**
     * This is the URLLoader callback method the MOBY data fetcher
     * will call when MOBY XML data is ready to be seen.
     */
    public void gotoURL(URL url, boolean addToHistory){
	if(url == null){
	    logger.warn("Ignoring null URL in MobyContentPane.gotoURL()");
	    return;
	}

	hasFailed = false;
	setCursor(new Cursor(Cursor.WAIT_CURSOR));
	try {
	    // 1000 is initial, unimportant guess as to resulting HTML size
	    StringBuffer resultBuffer = new StringBuffer(1000); 
	    
	    // loading a static HTML page, doesn't need reformatting
	    String urlString = url.toString().toLowerCase();
	    String protocol = url.getProtocol();
	    URLConnection urlCon = url.openConnection();

	    boolean unformatted = false;
	    // See if it's a WSDL file.  If so, launch the service wrapper functionality
	    if(urlString.endsWith(".wsdl")){
		loadWSDL(url);
		isWrapping = true;
		setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
		return;  // don't load the file in the pane
	    }
	    else{
		isWrapping = false;
	    }

	    // It's XML that needs to be transformed to HTML
	    if(urlString.endsWith(".xml") ||
	       "text/xml".equals(urlCon.getContentType())){
		editorPane.setContentType("text/html");
		isContentsXML = true;
		// Is it the second stage of wrapping, where data results are being seen?
		if(urlString.indexOf(WRAPPING_RESULTFILE_PREFIX) != -1){
		    //System.err.println("Wrapping service results");
		    isWrapping = true;
		}
		else{
		    //System.err.println("Not wrapping service results, xml file name ("+
		    //	       urlString +
		    //	       ") does not contain " + WRAPPING_RESULTFILE_PREFIX);
		}
		
		// Tell the stylesheet the URL of the moby data (it will create xpointers to it)
		contentGUI.getTransformer().setParameter(XSL_DOC_SOURCE_PARAM, url.toString());
		// Do the actual transformation
		String htmlContents = "[Seahawk Internal Error]";
		try{
		    StringWriter stringWriter = new StringWriter(1000);
		    contentGUI.getTransformer().transform(new StreamSource(url.openStream()),
							  new StreamResult(stringWriter));
		    htmlContents = stringWriter.toString();
		    // NOTE: If you switch the registry in the middle of using Seahawk,
		    // the following might fail on documents in the history, as they were
		    // encoded according to a different registry than the current value of 
		    // SeahawkOptions.getRegistry().  If we really want to support multiple concurrent
		    // registries, there has to be a way to note the source registry for the data in the 
		    // document (preferred), or we must maintain a hash of the document:registry mappings
		    // for the session.
		    htmlContents = HTMLUtils.encapsulateBinaryData(url, htmlContents, SeahawkOptions.getRegistry());
		}
		catch(TransformerException te){
		    status.setText("Sorry!  Could not transform the MOBY data into presentation form");
		    logger.error("Sorry!  Could not transform the MOBY data into presentation form: " + te);
		    setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
		    return;  // don't load the file in the pane
		}
		catch(Exception e){
		    // check to see if it's wsdl without a .wsdl suffix...
		    String body = HTMLUtils.getURLContents(url);
		    if(body.indexOf("definitions ") != -1 &&
		       body.indexOf("\"http://schemas.xmlsoap.org/wsdl/\"") != -1){
			loadWSDL(url);
			isWrapping = true;
		    }
		    // TODO if the data in XML that can be converted to MOBY...
		    // else if(){
		    //}
		    else{
			status.setText("Note: There was an error transforming the MOBY data " +
				       "into presentation form, it may not be displayed correctly");
			logger.error("Note: There was an error transforming the MOBY data " +
				     "into presentation form, it may not be displayed correctly", e);
		    }
		    setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
		    return;
		}
		// Get rid of inter-tag whitespaces
		htmlContents = htmlContents.replaceAll(">[ \\t\\r\\n]+<", "><");
		int origHash = htmlContents.hashCode();
		// Looks like tab-formatted text (at least 3 tabs)?
		htmlContents = htmlContents.replaceAll(">(([^<]*\t){3,}[^<]+)<", "><pre>$1</pre><");
		// Looks like formatting whitespace?
		htmlContents = htmlContents.replaceAll(">([^<]*       [^<]*)<", "><pre>$1</pre><");
		// If no preformatting done, break up long DNA stretches
		if(htmlContents.hashCode() == origHash){
		    // Break up any really long DNA/AA stretches
		    htmlContents = htmlContents.replaceAll("([a-zA-Z]{80})", "\n<br><tt>$1</tt>");
		    htmlContents = htmlContents.replaceAll("([a-zA-Z]{80}</tt>)([a-zA-Z]{1,})", "$1\n<br><tt>$2</tt><br>\n");		 
		}
		// Otherwise assume it's formatted
		resultBuffer.append(htmlContents);
	    }
	    // Assume HTML if suffix is one of the below
	    else if((protocol.equals("file") || protocol.equals("jar")) &&
		    (urlString.lastIndexOf(".xhtml") == urlString.length()-6 ||
		    urlString.lastIndexOf(".html") == urlString.length()-5 ||
		    urlString.lastIndexOf(".htm") == urlString.length()-4) ||
		    urlString.lastIndexOf(".tex") == urlString.length()-4){
		editorPane.setContentType("text/html");
		isContentsXML = false;
		if(urlString.lastIndexOf(".tex") == urlString.length()-4){
		    resultBuffer.append(TEX2HTML.convert(HTMLUtils.getInputStreamContents(urlCon.getInputStream())));
		}
		else{
		    resultBuffer.append(HTMLUtils.getInputStreamContents(urlCon.getInputStream()));
		    // Check if it's a form to wrap with Daggoo
		    if(shouldWrapHTMLForm(resultBuffer.toString())){
			loadWebForm(url);
			isWrapping = true;
			setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
			return;  // don't load the file in the pane
		    }
		}
	    }
	    // Maybe RTF?
	    else if(protocol.equals("file") &&
		    urlString.lastIndexOf(".rtf") == urlString.length()-4 ||
		    "text/richtext".equals(urlCon.getContentType()) ||
		    "text/rtf".equals(urlCon.getContentType())){
		// Put the results in the RTF viewer
		isContentsXML = false;
		editorPane.setContentType("text/rtf");
		resultBuffer.append(HTMLUtils.getInputStreamContents(urlCon.getInputStream()));		
	    }
	    // If we got here, we are either a file not ending in an XML, HTML, or RTF suffix,
	    // Or we are a non text/xml content-type Web URL.
	    // Assume it's plain text, or whatever the JEditorPane can glean from the content-type
	    // such as a remote HTML or RTF doc, because result buffer will be empty in condition below
	    else{
		if(MobySaveDialog.isConvertible(urlCon)){
		    urlCon = MobySaveDialog.convertToDisplayable(urlCon);
		    url = urlCon.getURL();
		}
		//editorPane.setContentType("text/plain");
		isContentsXML = false;
		//editorPane.setFont(Font.getFont("Monospaced"));
		editorPane.setContentType("text/html");
		String body = HTMLUtils.getURLContents(url);
		if(body.toLowerCase().indexOf("<html") != -1){
		    resultBuffer = null;
		    // Check if it's a form to wrap with Daggoo
		    if(shouldWrapHTMLForm(body)){
			loadWebForm(url);
			isWrapping = true;
			setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
			return;  // don't load the file in the pane
		    }
		}
		else{
		    // one last check to see if it's wsdl without a .wsdl suffix...
		    if(body.indexOf("definitions ") != -1 &&
		       body.indexOf("\"http://schemas.xmlsoap.org/wsdl/\"") != -1){
			loadWSDL(url);
			isWrapping = true;
			setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
			return;  // don't load the file in the pane
		    }
		    resultBuffer = new StringBuffer("<html><body><pre>"+
						    body.replaceAll("&", "&amp;").replaceAll("<","&lt;")+
						    "</pre></body></html>");
		}
	    }

	    if(resultBuffer == null){ // only happens if the url is already an HTML doc
		editorPane.setPage(url);
	    }
	    else if(resultBuffer.length() == 0 || unformatted){
		// Attempt to use MobyClient to convert the url's data into a Moby object
		// (e.g. if the file to load is an image, or a chromatogram) 
		try{
		    MobyContentInstance mobyContents = 
			MobyUtils.convertURLtoMobyBinaryData(servicesGUI.getMobyClient(), url);
		    if(mobyContents != null){
			File mobyRepresentation = File.createTempFile("seahawkBase64BinData", ".xml");
			mobyRepresentation.deleteOnExit();
			FileWriter fw = new FileWriter(mobyRepresentation);
			MobyDataUtils.toXMLDocument(fw, mobyContents);
			fw.close();
			// Recursive call with new URL, should be displayed using the MOBY XSLT
			gotoURL(mobyRepresentation.toURI().toURL(), addToHistory);
			setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
			return;
		    }
		} catch(Exception e){
		    e.printStackTrace();
		    editorPane.setText("Failure in loading MOBYfied binary data:\n" + e);
		    logger.error("Failure in loading MOBYfied binary data:\n" + e);
		}
		// Shouldn't get here unless either not binary, or the binary data load failed
		if(resultBuffer.length() == 0){
		    editorPane.setPage(url);
		} else{
		    editorPane.setText(resultBuffer.toString());
		}
	    }
	    else{
		editorPane.setText(resultBuffer.toString());
		// The following line is so relative URLs are resolved correctly
		editorPane.getDocument().putProperty(javax.swing.text.Document.StreamDescriptionProperty, url);
	    }
	    // Regardless of the doc, scroll to a reference called "start:" if available.
	    // By default the pane will annoyingly scroll to the bottom when new data is loaded.
	    editorPane.scrollToReference("start");
	}
	catch (Exception e) {
	    status.setText("Warning: Could not read transformed (HTML) response from MOBY, or given panel contents: " + e);
	    e.printStackTrace();
	}

	setCursor(new Cursor(Cursor.DEFAULT_CURSOR));

	if(addToHistory){
	    // Are we truncating the history? (i.e. loading a new doc in the middle of the history)
	    if(historyIndex != history.size()-1){
		history.setSize(historyIndex+1);
	    }
	    history.add(url);
	    
	    historyIndex = history.size()-1;
	    contentGUI.updateHistory(this);
	}

	// This page has had a title associated with it before...reuse it!
	if(historyTabLabels.containsKey(url)){
	    tabbedPane.setTitleAt(tabbedPane.indexOfComponent(this), historyTabLabels.get(url));
	}
	// If this page has a filter associated with it before reload it
	try{
	    currentDoc = null;  // get rid of previous in-memory doc use for interactive filtering, if any
	    currentSelectedData.clear();
	    currentSelectionXPath = new StringBuffer();
	    filterableNodes = null;
	    origStyles = null;
	    firstDocRendering = true;
	    editorPane.getHighlighter().removeAllHighlights(); 

	    if(filterHistory.containsKey(url)){
		setFilterVisible(true);
	    }
	    else{
		setFilterVisible(false);
	    }
	    filterChanged = false;
	} catch(Exception e){
	    logger.warn("Could not enable filter history for this document: "+e.getMessage(), e);
	}
    }

    public int getHistorySize(){
	return history.size();
    }

    public int getHistoryIndex(){
	return historyIndex;
    }

    public URL getCurrentURL(){
	if(historyIndex < 0 || historyIndex > history.size()-1){
	    return null;
	}
	return history.elementAt(historyIndex);
    }

    protected URL getPreviousURL(){
	if(historyIndex < 1 || historyIndex > history.size()){
	    return null;
	}
	return history.elementAt(historyIndex-1);
    }

    public boolean canFilter(){
	return hasXMLSource();
    }

    public boolean canGoBack(){
	return historyIndex > 0;
    }
    
    public boolean canGoForward(){
	return historyIndex < history.size()-1 && isFilterForwardConsistent();
    }

    /**
     * If the filter condition has changed since the next service was originally called,
     * only allow navigation to that document (service results) again if the new filter allows 
     * the data used in the original call to pass (for logical dataflow consistency).
     */
    public boolean isFilterForwardConsistent(){
	// If there is no forward doc, it is consistent I suppose
	if(historyIndex == history.size()-1){
	    contentGUI.setForwardButtonToolTip(null);
	    return true;
	}
	// If no filter on current doc, we must be okay to go forward
	if(getFilter() == null){
	    contentGUI.setForwardButtonToolTip(null);
	    return true;
	}

	// Get the data from this doc used as the input to the forward document
        if(xPtrsReferencedInNextService == null){
	    try{
		xPtrsReferencedInNextService = DataUtils.getInputXPtrs(history.elementAt(historyIndex+1), history.elementAt(historyIndex));
	    } catch(Exception e){
		logger.error("Cannot get the provenance data from "+history.elementAt(historyIndex+1), e);
                return false;
	    }
	    
	}
	// See if all the particular data from this doc that was used in the next service is still avaialble after
	// applying the current version of the filter
	for(String usedxPtr: xPtrsReferencedInNextService){
	    for(String filteredXPtr: filteredData.keySet()){
		if(usedxPtr.equals(filteredXPtr) ||
		   usedxPtr.startsWith(filteredXPtr+"/")){
		    // the data used in the next service is filtered in the current view, not consistent to allow forward button
		    // give the person a reason the forward is disabled
		    contentGUI.setForwardButtonToolTip("<html>Cannot go forward to next doc, the corresponding service<br>"+
						       "depended on input data that is now filtered.  Reset the data filter<br>"+
						       "to be able to go forward again</html>");
		    return false;  
		}
	    }
	}

	// If they differ, check that the forward doc's input src is not filtered out currently
	contentGUI.setForwardButtonToolTip(null);
	return true;
    }

    public void goForward(){
	if(canGoForward()){
	    if(filterChanged){
		DataUtils.updateInputFilter(history.elementAt(historyIndex+1), history.elementAt(historyIndex), getFilter());
	    }
	    gotoURL(history.elementAt(++historyIndex), false);
	    xPtrsReferencedInNextService = null;
	}
    }

    public void goBackward(){
	if(canGoBack()){
	    xPtrsReferencedInNextService = null;
	    gotoURL(history.elementAt(--historyIndex), false);
	}
    }
    
    /**
     * Called after data copied from this pane is pasted.  The actual text pasted in given
     * so that it can be unified with the object that created it (lastURLDragged) in the 
     * PBE recorder.
     */
    public void exportDone(String valuePasted, String transformRuleURI){
	if(cgiServlet != null && cgiServlet.getRecorder() != null && (cgiServlet.getRecorder() instanceof PBERecorder)){
	    ((PBERecorder) cgiServlet.getRecorder()).dataCopied(getDraggedData(), valuePasted, transformRuleURI);
	}
	if(soapServlet != null && soapServlet.getRecorder() != null && (soapServlet.getRecorder() instanceof PBERecorder)){
	    ((PBERecorder) soapServlet.getRecorder()).dataCopied(getDraggedData(), valuePasted, transformRuleURI);
	}
    }

    public int getExportOption(String[] options){
	if(cgiServlet == null && soapServlet == null){
	    logger.warn("Not showing GUI for export options: CGI and SOAP servlets are null");
	    return -1;
	}
	
	if(cgiServlet != null && cgiServlet.getRecorder() != null){
	    if(!(cgiServlet.getRecorder() instanceof PBERecorder)){
		logger.warn("Not showing GUI for export options: CGI servlet recorder was not " +
			    "of expected class PBERecorder: found " + cgiServlet.getRecorder().getClass().getName());
		return -1;
	    }
	    return ((PBERecorder) cgiServlet.getRecorder()).getChoice("Choose a format for the pasted data:", options);
	}
	else if(soapServlet != null && soapServlet.getRecorder() != null){
	    if(!(soapServlet.getRecorder() instanceof PBERecorder)){
		logger.warn("Not showing GUI for export options: SOAP servlet recorder was not " +
			    "of expected class PBERecorder: found " + soapServlet.getRecorder().getClass().getName());
		return -1;
	    }
	    return ((PBERecorder) soapServlet.getRecorder()).getChoice("Choose a format for the pasted data:", options);
	}
	else{
	    logger.warn("Not showing GUI for export options: neither the CGI nor the SOAP servlet recorder is set");
	    return -1;
	}
    }

    /**
     * Called by MOBYRequest when the service request is being sent.  We
     * can create the GUI and wait message here.
     */
    public void start(MobyRequestEvent requestEvent){
	hasFailed = false;
	tabbedPane.setSelectedComponent(this);
	tabbedPane.setTitleAt(tabbedPane.indexOfComponent(this), 
			      ""+requestEvent.getID()+"-"+requestEvent.getService().getName());
	setWaitScreen();
    }

    public void stop(MobyRequest request, int requestID){
    }

    // Called by MobyRequest when the response is available
    public void processEvent(MobyRequestEvent mre){

	Throwable e = mre.getException();
	if(e != null){
	    failed("Exception occurred");
	    // There was an exception, figure out the type
	    if(e instanceof MobyException){
		editorPane.setText("Failure in MOBY protocol:\n" + e);
		logger.error("Failure in MOBY protocol:\n" + e);
		return;
	    }
	    else if(e instanceof SOAPException){
		editorPane.setText("Failure in SOAP transaction:\n" + e);
		logger.error("Failure in SOAP transaction:\n" + e);
		return;
	    }
	    else if(e instanceof NoSuccessException){
		editorPane.setText("Failure in MOBY logic (input was not acceptable for this service):\n" + e);
		logger.error("Failure in MOBY logic (input was not acceptable for this service):\n" + e);
		return;
	    }
	    else{
		editorPane.setText("General Failure during MOBY action:\n" + e);
		logger.error("General Failure during MOBY action:\n" + e);
		e.printStackTrace();
		return;
	    }	    
	}

	// Output was okay
	loadDataInBrowser(mre);
    }

    public void failed(String msg){
	int tabIndex = tabbedPane.indexOfComponent(this);
	if(tabIndex > -1){
	    try{tabbedPane.setIconAt(tabIndex, 
				     new CloseTabIcon(failedIcon, TAB_ICON_SPACER));}
	    catch(Exception e){e.printStackTrace(); 
	        tabbedPane.setIconAt(tabIndex, failedIcon);}
	}
	else{
	    tabbedPane.setIconAt(tabIndex, failedIcon);
	}
	tabbedPane.setDisabledIconAt(tabIndex, failedIcon);

	tabbedPane.setTitleAt(tabIndex, msg);
	// Add the ability to back track from the error to the previous document
	if(historyIndex != -1){
	    historyIndex++;
	    contentGUI.updateHistory(this);
	}
	isContentsXML = false;
	hasFailed = true;
    }

    public void succeeded(String msg){
	int tabIndex = tabbedPane.indexOfComponent(this);
	if(tabIndex > -1 && tabbedPane.getTabCount() > 1){
	    try{tabbedPane.setIconAt(tabIndex, 
				     new CloseTabIcon(loadedIcon, TAB_ICON_SPACER));}
            catch(Exception e){e.printStackTrace(); 
	        tabbedPane.setIconAt(tabIndex, loadedIcon);}
	}
	else{
	    tabbedPane.setIconAt(tabIndex, loadedIcon);
	}
	tabbedPane.setDisabledIconAt(tabIndex, loadedIcon);
	tabbedPane.setTitleAt(tabIndex, msg);
	// Store the latest title for the URL loaded, in case we revisit
	if(historyIndex != -1){
	    historyTabLabels.put(history.elementAt(historyIndex), msg);
	}
	hasFailed = false;
    }

    public boolean hasFailure(){
	return hasFailed;
    }

    // Save the MOBY data to a file, then tell NewBrowser to load that local file URL
    protected void loadDataInBrowser(MobyRequestEvent mre){
	
	MobyRequest mobyRequest = mre.getSource();

	MobyContentInstance responses = mre.getContent();

	// Make sure the run has something to show
	if(responses == null || responses.size() == 0){
	    failed("No results");
	    editorPane.setText("Warning: MOBY Service " + mobyRequest.getService().getName() + 
			   " did not return any results\n");
	    
	    return;
	}

	String responseType = getResponseType(responses);
	boolean hasContents = hasContents(responses);

	// Maybe there's no data, but there is an exception?
	if(!hasContents){
	    ServiceException[] exs = responses.getExceptions();
	    if(exs != null && exs.length != 0){
		hasContents = true;
	    }
	}

	if(!hasContents){
	    failed("Empty response");
	    editorPane.setText("Sorry: " + mobyRequest.getService().getName() + 
			       " executed successfully, but did not return data\n");
	    return;
	}

	try {
            URL outputURL = DataUtils.saveOutputData(mre);
            gotoURL(outputURL, true);
	} catch (Exception e) {
	    failed("Could not write a local file");
	    editorPane.setText("ERROR: Could not write temporary file for MOBY results: " + e);
	    e.printStackTrace();
	    return;
	}
	succeeded(responseType);
    }

    protected boolean hasContents(MobyContentInstance responses){
        MobyDataObjectSet retrievedObjects = responses.retrieveObjects();
	return retrievedObjects != null && retrievedObjects.size() > 0;
    }

    /**
     * Creates a human readbale string describing the responses contents.  Specifically, 
     * the number of, and common type and namespace of the data if available.
     */
    protected String getResponseType(MobyContentInstance responses){
	// Make sure the response had useful data in it (i.e. at least one simple)
	Iterator i = responses.keySet().iterator();
	String responseType = "Empty";
	while(i.hasNext()){
	    String responseName = (String) i.next();

	    AbstractMap response = (AbstractMap) responses.get(responseName);
	    if(response == null){
		logger.warn("Skipping null response " + responseName);
	    }
	    Iterator values = response.keySet().iterator();
	    while(values.hasNext()){
		String dataName = (String) values.next();
		Object data = response.get(dataName);
		if(data == null){
		    logger.warn("Skipping null data '" + dataName + "' in response" + responseName);
		}
		else if(data instanceof MobyPrimaryDataSimple){
		    MobyDataType dataType = ((MobyPrimaryDataSimple) data).getDataType();
		    if(dataType == null){
			responseType = "Object";
		    }
		    else{
			responseType = ((MobyPrimaryDataSimple) data).getDataType().getName();
		    }
		    String namespace = getCommonNamespace((MobyPrimaryDataSimple) data);
		    if(namespace != null){
			responseType += " ("+namespace+")";
		    }
		    break;
		}
		else if(data instanceof MobyPrimaryDataSet &&
			((MobyPrimaryDataSet) data).getElements().length > 0){
		    MobyDataType dataType = ((MobyPrimaryDataSet) data).getDataType();
		    if(dataType == null){
			responseType = "Object";
		    }
		    else{
			responseType = ((MobyPrimaryDataSet) data).getDataType().getName();
		    }
		    if(((MobyPrimaryDataSet) data).getElements().length == 1){
			char c = responseType.toLowerCase().charAt(0);
			responseType = "A" + (c=='a'||c=='e'||c=='i'||c=='o'||c=='u'?"n":"") + 
			    " " + responseType;
		    }
		    else{
			responseType = ((MobyPrimaryDataSet) data).getElements().length + 
			    " " + responseType + "s";
		    }
		    String namespace = getCommonNamespace((MobyPrimaryDataSet) data);
		    if(namespace != null){
			responseType += " ("+namespace+")";
		    }
		    break;
		}
	    }
	}

	return responseType;
    }

    /**
     * If all data passed in contain share a namespace, that string is returned, 
     * otherwise null is returned.
     */
    protected String getCommonNamespace(MobyPrimaryData data){
	MobyNamespace[] ns = data.getNamespaces();
	if(ns != null && ns.length == 1 && ns[0] != null && ns[0].getName() != null
	   && ns[0].getName().length() != 0){
	    return ns[0].getName();
	}
	else{
	    return null;
	}
    }

    /**
     * The editor pane calls back here when the user flies over or clicks on a hyperlink
     */
    public void hyperlinkUpdate(HyperlinkEvent he){
	URL targetURL = he.getURL();
	if(he.getEventType() == HyperlinkEvent.EventType.ACTIVATED){
	    // Internal moby pseudo-URL
	    if(targetURL == null){
		String desc = he.getDescription();
		if(desc.startsWith(MobyContentGUI.SERVICE_INPUT_MAGIC)){
		    // Run the input as is on the specified service.  Desc is "#!inputURL\tserviceAuthURI\tserviceName"
		    String[] runSpec = desc.substring(MobyContentGUI.SERVICE_INPUT_MAGIC.length()).split("\t");
		    URL inputURL = null;
		    try{
			inputURL = new URL(runSpec[0]);
		    }
		    catch(Exception e){
			logger.warn("Could not parse service input file URL "+runSpec[0], e); 
			return;
		    }
		    try{
			callServiceForInputURL(runSpec[1], runSpec[2], inputURL);
		    } catch(Exception e){
			logger.error("Could not call service "+runSpec[2]+" for peer set", e);
			return;
		    }
		}
		else if(desc.startsWith("#")){
		    // Anchor hyperlink
		    editorPane.scrollToReference(desc.substring(1));
		}
		else{
		    status.setText("Failed: could not parse hyperlink: " + he.getDescription());
		    logger.warn("Failed: could not parse hyperlink: " + he.getDescription());
		}
	    }
	    else if(targetURL.getHost() == null){
		status.setText("Failed: could not get hyperlink host: " + targetURL);
		logger.warn("Failed: could not get hyperlink host: " + targetURL);
	    }
	    else if(isMobyURL(targetURL)){
		if(isLinkEnabled(targetURL)){
		    status.setText("Retrieving service list for selected data");
		    showMobyOptions(targetURL);
		}
		else{
		    status.setText("Change the data filter to re-enable this link");
		}
	    }
	    // External, hopefully HTML link
	    else{
		status.setText("Launching Web browser for external link...");
		launchInWebBrowser(targetURL);
	    }
	    
	}
	else if(he.getEventType() == HyperlinkEvent.EventType.ENTERED){
	    overHyperlink = true;
	    lastHyperlinkHovered = targetURL;
	    if(isMobyURL(targetURL)){
		if(isLinkEnabled(targetURL)){
		    status.setText("Click to discover more Moby services, or drag onto a Web form");
		}
		else{
		    status.setText("This link is current disabled because of the data filter.");
		}
	    }
	    else if(targetURL == null){
		String desc = he.getDescription();
		if(desc.startsWith(MobyContentGUI.SERVICE_INPUT_MAGIC)){
		    status.setText("Click to run service on dataset");
		}
		// # start means relative anchor
		else if(desc.startsWith("#")){
		    status.setText("Scroll to section '" + desc.substring(1) + "'");
		}
	    }
	    else{
		status.setText("Launch external link " + targetURL);
	    }
	}
	else if(he.getEventType() == HyperlinkEvent.EventType.EXITED){
	    overHyperlink = false;
	    status.setText("");
	}
    }

    public MobyContentGUI getContentGUI(){
	return contentGUI;
    }

    private void callServiceForInputURL(String providerURI, String serviceName, URL inputURL) throws Exception{
	MobyContentInstance inEnvelope = MobyDataUtils.fromXMLDocument(inputURL);
	MobyService service = contentGUI.findService(providerURI, serviceName);
 	MobyContentInstance result = contentGUI.callService(service, inEnvelope);

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

    private boolean isLinkEnabled(URL url){
	String ref = url.getRef();
	if(ref == null || ref.length() == 0){
	    return true;
	}
	for(String disabledXPtr: filteredData.keySet()){
	    if(ref.equals(disabledXPtr) || ref.startsWith(disabledXPtr+"/")){
		return false;
	    }
	}
	return true;
    }

    public MobyDataInstance getDraggedData(){
	return urlToMobyData(lastHyperlinkDragged);
    }

    protected MobyDataInstance urlToMobyData(URL targetURL){
	// Build a MOBY object using the URL info
	// Links have the form moby_xml_url#child_xpath refering to complex objects
	// or http://moby/namespace?id=moby_id&string=cdata_value for simple objects and strings

	String docFragID = targetURL.getRef();  // We store the xpath in the anchor part of the URL

	// Complex case, load the doc fragment from the MOBY XML source file 
	// where the targetURL is url#xptr with xptr like /1/2/1/1
	if(docFragID != null && docFragID.length() > 0 && !targetURL.getHost().equals("moby")){
	    URL currentURL = null;
	    try{currentURL = new URL(targetURL.toString().replaceAll("#"+targetURL.getRef(), ""));}
	    catch(Exception e){
		logger.error("Couldn't extract referenceless URL from " + targetURL, e);
	    }
	    // refers to currently loaded doc
	    if(currentURL.equals(getCurrentURL())){
		return DataUtils.loadMobyDataFromXPointer(currentURL, getCurrentDoc(), docFragID, 
							  filteredData, filterHistory.get(getCurrentURL()));
	    }
	    // refers to another moby doc, let DataUtils resolve the doc for us
	    else{
		return DataUtils.loadMobyDataFromXPointer(targetURL, filteredData, filterHistory.get(getCurrentURL()));
	    }
	}

	// Simple case, build the object using the values encoded in the URL itself.
	// Currently this is not used because it messes up provenance tracking for PbE
	else{
	    return loadMobyDataFromURLQuery(targetURL);
	}
    }

    protected void showMobyOptions(URL targetURL){
	MobyDataInstance mobyData = urlToMobyData(targetURL);

	if(mobyData == null){
	    logger.warn("Cannot create MOBY service list, could " +
			       "not extract MOBY Data Object from " +targetURL);
	    return;
	}
	if(!(mobyData instanceof MobyPrimaryData)){
	    logger.warn("Cannot create MOBY service list for non-primary " +
			"MOBY Data (was " +mobyData.getClass().getName()+")");
	    return;
	}

	JPopupMenu popup = new DynamicJPopupMenu();
	popup.setName(MOBY_SERVICE_POPUP_NAME);
	addExtraMobyOptions(popup, mobyData, targetURL);
	JMenuItem checkingLinksPopupItem = new JMenuItem("Checking for services...");
	popup.add(checkingLinksPopupItem);
	popup.show(this, lastClickX+editorPane.getX(), lastClickY+editorPane.getY());
	servicesGUI.addPopupOptions(mobyData, popup, true, getDefaultHandler()); // true = asynchronous

	// It's possible that the current node clicked has peers that could also be submitted at the same time.
	// In this case, create a "for each" option to submit all peers to the service.  We don't actually build
	// the input data, but provde a callback listener to create the full submission envelope if the user 
	// chooses the for each option.  The Moby data instance's userData gives us the current context, enabling us 
        // to construct the correct payload in MobyPayloadCreator.createPayload() at any point in the future.
        String origUserData = (String) mobyData.getUserData();
        for(String peerMode: new String[]{DataUtils.ARTICLE_PEERS_MODE, DataUtils.DATATYPE_PEERS_MODE}){
            // note that getPeerElements() will modify the userData to reflect the semantic of the requested peer mode
            mobyData.setUserData(origUserData);
            NodeList peerElements = DataUtils.getPeerElements(getCurrentDoc(), mobyData, getFilteredData(), peerMode);
            if(peerElements == null || peerElements.getLength() <= 1){
                continue; // no 'for each' option if only one to process...
            }
            String each = "each <i>(" + peerElements.getLength() + " total)</i>";
            if(peerMode.equals(DataUtils.ARTICLE_PEERS_MODE)){
                each += " '"+mobyData.getName()+"'";
            }
            MobyDataType dataType = ((MobyPrimaryData) mobyData).getDataType();
	    MobyNamespace[] namespaces = ((MobyPrimaryData) mobyData).getNamespaces();
            if(mobyData instanceof MobyPrimaryDataSet){	 // Collection
                servicesGUI.addPopupOptions((MobyPrimaryDataSet) mobyData, popup, true, getDefaultHandler(), 
					    new MobyPayloadCreator(this, (String) mobyData.getUserData()), 
					    each);
            }
            else if(MobyTags.MOBYOBJECT.equals(dataType.getName())){  
		if(namespaces != null && namespaces.length > 0){
		    servicesGUI.addPopupOptions(namespaces[0], popup, true, getDefaultHandler(),
						new MobyPayloadCreator(this, (String) mobyData.getUserData()), 
						each);
		}
		//else base object wth no namespace...useless!
            }
            else{
		// A non-base object, with a namespace too
		if(namespaces != null && namespaces.length > 0){
		    ((MobyPrimaryDataSimple) mobyData).setId(""); //anonymize
		    servicesGUI.addPopupOptions((MobyPrimaryDataSimple) mobyData, popup, true, getDefaultHandler(),
						new MobyPayloadCreator(this, (String) mobyData.getUserData()), 
						each);
		}
		else{  //just the data type useful
		    servicesGUI.addPopupOptions(dataType, popup, true, getDefaultHandler(),
						new MobyPayloadCreator(this, (String) mobyData.getUserData()), 
						each);
		}
            }
        }

	status.setText("Shift+Click=Service in new tab, Ctrl+Click=Use default 2nd params");
	if(popup.getSubElements().length > 0){
	    popup.setSelected(null);
	    popup.setVisible(false);
	    popup.remove(checkingLinksPopupItem);
	    if(popup.getSubElements().length > 0){
		popup.setVisible(true);
	    }
	}
    }

    Map<String,String> getFilteredData(){
      return filteredData;
    }

    Map<String,Boolean> getJobXPtrs(){
      return jobXPtrs;
    }


    /**
     * Subclasses can use this method to expand the popup menu.  Be sure to call super.addExtraMobyOptions()
     */
    protected void addExtraMobyOptions(JPopupMenu popup, MobyDataInstance mdi, URL srcURL){
	// If we are wrapping a service, and you're getting the context menu, you must be 
	// looking at service results, in which case let's give the option to create a service.
	if(isWrapping){
	    itemToReturn = mdi;
	    popup.add(createServicePopupItem);
	    popup.setVisible(true);	    
	}
	// The item below is mainly to allow X to be used in service Y if running service Z on X meets some filter criteria.
	// This is a common task in bioinformatics workflows.
	// Backtrack from data to input used for service that generated the data.

	// Here's an important point: if there is no filter on the current doc, we shouldn't be offering
	// a "previous input" option, because the current doc's service contributed nothing to deciding 
	// on how to proceed (no "if" conditon).
	if(getFilter() == null){
	    return;
	}
	String[] userData = mdi.getUserData() == null ? null : 
	    mdi.getUserData().toString().split("\t");
	// second user data arg is 
	Node mdiNode = userData == null || userData.length < 2 ? null : 
	    XPointerResolver.getNodeFromXPointer(getCurrentDoc(), userData[1]);

	//System.err.println("Should create a previous input menu item for " + mdi.getUserData());
	try{
	    Document srcDoc = DataUtils.docBuilder.parse(srcURL.openStream());

	    //System.err.println("Context node is "+mdiNode+ " from user data" + mdi.getUserData());
	    // Get the input data for the *previous* service
	    MobyDataJob inputData = DataUtils.getInputSample(srcDoc, 
							     DataUtils.findMobyJobName(mdiNode), 
							     SeahawkOptions.getRegistry());
	    if(inputData == null){
		// should only happen if Moby XML was loaded manually by a user, otherwise we've recorded this info...
		logger.warn("No previous service info in Moby XML, 'previous input' option will not be displayed");
		return;
	    }
	    for(MobyDataInstance previousMDI: inputData.getPrimaryData()){		
		// add conditional of the current service as a criteria in user data for each previousMDI, 
		// so PbE will properly reflect the logic of if(f1(x) matches filter){f2(x)}
		DataUtils.addUserData(previousMDI, srcURL, getFilter());

		servicesGUI.addPopupOptions(previousMDI, popup, true, null, null, "previous input");
	    }
	} catch(Exception e){
	    logger.warn("Could not load previous service info, 'previous input' option will not be displayed", e);
	}

    }

    /**
     * If a service is requested from a link on this pane, the object returned here
     * will get the callback.  Normally, the reference is to "this", as new responses should
     * load in the same pane as the current data.  Returning null would mean that MobyServiceGUI's
     * default handler should be used.
     */
    protected MobyRequestEventHandler getDefaultHandler(){
	return this;
    }

    public void setWaitScreen(){
	// Put the message that the job is running in the HTML viewer
	// so people know it's working
	editorPane.setContentType("text/plain");
	editorPane.setText("Your request is being processed. You can continue exploring in " +
			   "other tabs and windows while I wait for the results.");

	// Set the icon for the tab to an hourglass
	if(tabbedPane != null){
	    int tabIndex = tabbedPane.indexOfComponent(this);
	    if(tabIndex > -1){
		try{tabbedPane.setIconAt(tabIndex, 
					 new CloseTabIcon(hourglassIcon, TAB_ICON_SPACER));}
		catch(Exception e){e.printStackTrace(); 
	            tabbedPane.setIconAt(tabIndex, hourglassIcon);}
	    }
	    else{
		tabbedPane.setIconAt(tabIndex, hourglassIcon);
	    }
	    tabbedPane.setDisabledIconAt(tabIndex, hourglassIcon);
	}
    }

    protected MobyDataObject loadMobyDataFromURLQuery(URL targetURL){
	String namespace = targetURL.getPath(); // We store the namespace in the path part of the URL
	// Get rid of leading / in path
	namespace = namespace.substring(1);
	String query = targetURL.getQuery();
	String mobyID = null;
	String mobyString = null;
	String mobyName = "";

	MobyDataObject mobyData = null;

	// extract the ID or string value
	if(query != null && query.length() > 0){
	    StringTokenizer queryTokenizer = new StringTokenizer(query, "&");
	    while(queryTokenizer.hasMoreTokens()){
		String token = queryTokenizer.nextToken();
		if(token != null){
		    if(mobyID == null && token.startsWith("id=")){
			if(token.length() > 3){
			    mobyID = token.substring(3);
			}
			else{
			    mobyID = "";
			}
		    }
		    else if(mobyString == null && token.startsWith("string=") && token.length() >= 7){
			if(token.length() > 7){
			    mobyString = token.substring(7);
			}
			else{
			    mobyString = "";
			}
		    }
		    else if(mobyName.length() == 0 && token.startsWith("name=") && token.length() >= 5){
			if(token.length() > 5){
			    mobyName = token.substring(5);
			    // The name may also contain #xxx, which will be in the ref part of 
			    // the URL and needs to be appended.
			    if(targetURL.getRef() != null){
				mobyName += "#"+targetURL.getRef();
			    }
			}
			else{
			    mobyName = "";
			}
		    }
		    else{
			logger.warn("Warning: unrecognized or duplicate query part '" + 
				    token + "' in moby link " + targetURL);
		    }
		}
	    }
	}
	
	if(mobyString != null){
	    mobyData = new MobyDataString(mobyName, mobyString, SeahawkOptions.getRegistry());
	}
	else{
	    mobyData = new MobyDataObject(mobyName, SeahawkOptions.getRegistry());
	}
	mobyData.addNamespace(MobyNamespace.getNamespace(namespace, SeahawkOptions.getRegistry()));
	
	if(mobyID != null){
	    mobyData.setId(mobyID);
	}
	
	return mobyData;
    }
    

    /**
     * Gets the filter criteria currently being applied to the document being viewed.
     *
     * @return null unless there is a currently a non-blank filter criteria
     */
    public FilterSearch getFilter(){
	if(getCurrentURL() == null){
	    return null;
	}
	FilterSearch fs = filterHistory.get(getCurrentURL());
	if(fs != null && fs.getFilterRegex().length() > 0){
	    return fs;
	}
	return null;
    }

    protected Document getCurrentDoc(){
	if(currentDoc == null && getCurrentURL() != null){  // parse the doc into memory if not already there
	    try{
		currentDoc = DataUtils.docBuilder.parse(getCurrentURL().openStream());
	    } catch(Exception e){
		logger.error("Could not parse Moby document " + getCurrentURL(), e);
		return null;
	    }
	}
        return currentDoc;
    }

    /**
     * Determine the parts of the XML document matching the FilterSearch criteria for the current doc, 
     * and filter the view accordingly.  Used for interactive filtering in the display.
     */
    protected void applyFilter(boolean apply){
	if(!filterChanged){
	    filterChanged = true;
	}

        FilterSearch filter = filterHistory.get(getCurrentURL());
	filterableNodes = DataUtils.findFilteredNodes(getCurrentDoc(), filter, filterableNodes, filteredData, jobXPtrs,
                                                      currentSelectedData, currentSelectionXPath, apply);
 
	if((!apply || filter == null || filter.getFilterRegex().toString().length() == 0) && firstDocRendering){
	    // Show whole doc if no filter or if filter is blank, so nothing to do here
	    firstDocRendering = false;
	}
	// apply gray out in the document display, or need to cleanup previous state even if no filter now
        else{
	    displayFilterEffect();
        }
    }


    /**
     * Gray out parts of the HTML view if they are part of the filtered data.
     */
    private void displayFilterEffect(){
	
	HTMLDocument d = (HTMLDocument) editorPane.getStyledDocument();

	if(origStyles == null){
 	    // Store a copy of the doc's default rendering for all elements with xpointer 
 	    // (i.e. filterable elements) before doing any filter display mods,
 	    // as this allows us to remove the filter easily without reloading the document.
 	    origStyles = new Vector<AttributeSet>();	    
 	    for(String filterable: filterableNodes){
 		javax.swing.text.Element el = d.getElement(filterable);
 		origStyles.add(d.getParagraphElement(el.getStartOffset()).getAttributes());
 	    }
 	    firstDocRendering = false;
 	}
 	// Restore the original document appearance before applying the current filter...
 	else{
 	    Iterator<AttributeSet> origAttIter = origStyles.iterator();
	    // Restore text data
  	    for(String filterable: filterableNodes){
 		javax.swing.text.AbstractDocument.AbstractElement el = 
		    (javax.swing.text.AbstractDocument.AbstractElement) d.getElement(filterable);
		javax.swing.text.SimpleAttributeSet plain = new javax.swing.text.SimpleAttributeSet();
		d.setParagraphAttributes(el.getStartOffset(), el.getEndOffset()-el.getStartOffset()+1, origAttIter.next(), true);
 	    }
	    // Restore hyperlinks to default (blue underlined) appearance
	    javax.swing.text.SimpleAttributeSet blueAttrSet = new javax.swing.text.SimpleAttributeSet();
	    blueAttrSet.addAttribute(StyleConstants.Foreground, Color.blue);
	    blueAttrSet.addAttribute(StyleConstants.Underline, Boolean.TRUE);
	    for(HTMLDocument.Iterator it = d.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
		String link = (String) it.getAttributes().getAttribute(HTML.Attribute.HREF);
		if(link == null){
		    continue;
		}
		d.setCharacterAttributes(it.getStartOffset(), it.getEndOffset()-it.getStartOffset()+1, blueAttrSet, false);
	    }
	}

	javax.swing.text.SimpleAttributeSet grayAttrSet = new javax.swing.text.SimpleAttributeSet();
	grayAttrSet.addAttribute(StyleConstants.Foreground, Color.lightGray);
	grayAttrSet.addAttribute(StyleConstants.Underline, Boolean.FALSE);

	// Gray out hyperlinks that are for filtered data or their children
	for(HTMLDocument.Iterator it = d.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
	    String link = (String) it.getAttributes().getAttribute(HTML.Attribute.HREF);
	    if(link == null || link.indexOf("#") == -1){
		continue;
	    }
	    link = link.replaceFirst("^.*#(.*)$", "$1"); // just get the ref
	    
	    for(Map.Entry<String,String> dataToFilter: filteredData.entrySet()){
		if(link.equals(dataToFilter.getKey()) || link.startsWith(dataToFilter.getKey()+"/")){
		    d.setCharacterAttributes(it.getStartOffset(), it.getEndOffset()-it.getStartOffset()+1, grayAttrSet, false);
		}
	    }
	}

	// Gray out the mismatched textual data
	for(Map.Entry<String,String> dataToFilter: filteredData.entrySet()){	    

	    //System.err.println("Filtering "+dataToFilter.getKey());
	    javax.swing.text.AbstractDocument.AbstractElement htmlDiv = 
		(javax.swing.text.AbstractDocument.AbstractElement) d.getElement(dataToFilter.getKey());
	    if(htmlDiv == null){
		logger.error("Could not find HTML element with expected id "+dataToFilter.getKey());
		continue;
	    }
	    int areaStart = htmlDiv.getStartOffset();
	    int areaEnd = htmlDiv.getEndOffset();
	    d.setParagraphAttributes(areaStart, areaEnd-areaStart+1, grayAttrSet, false);
	}

	// Highlight the matches
	Highlighter h = editorPane.getHighlighter();
	h.removeAllHighlights(); //get rid of old highlights
	FilterSearch fs = filterHistory.get(getCurrentURL());
	int matchCount = 0;
	if(fs != null && fs.getFilterRegex().length() > 0){
	    try{
                Map<String,Boolean> highlights = new HashMap<String,Boolean>();
		javax.swing.text.LayeredHighlighter.LayerPainter painter = 
		    new DefaultHighlighter.DefaultHighlightPainter(Color.yellow);
		Pattern p = Pattern.compile(fs.getFilterRegex().toString(), 
				            Pattern.MULTILINE | Pattern.DOTALL | 
                                             (fs.getCaseSensitivity() ? 0 : Pattern.CASE_INSENSITIVE));
		for(String filterable: filterableNodes){
		    javax.swing.text.Element el = d.getElement(filterable);
		    String fragment = d.getText(el.getStartOffset(),
						el.getEndOffset() - el.getStartOffset());
		    Matcher matcher = p.matcher(fragment);
		    while (matcher.find()) {
			// Only count match if not already highlighted in a containing filterable node
			// todo: in future, avoid looking in text bits already checked, for efficiency
			int hstart = el.getStartOffset() + matcher.start();
                        int hend = el.getStartOffset() + matcher.end();
			if(!highlights.containsKey(""+hstart+","+hend)){
			    h.addHighlight(hstart, hend, painter);
			    matchCount++;
			    highlights.put(""+hstart+","+hend, Boolean.TRUE);
                        }	
		    }
		}
	    } catch(Exception e){
		logger.error("Could not apply match highlights", e);
	    }
	}
	status.setText(" Found "+matchCount+" match"+(matchCount == 1 ? "": "es"));

	// Update the ability to press the forward button, based on the new filter criteria
	contentGUI.updateHistoryButtons();
    }

    private List<javax.swing.text.Element> getHyperlinkElements(javax.swing.text.Element el){
	List<javax.swing.text.Element> links = new Vector<javax.swing.text.Element>();
	for(int i = 0; i < el.getElementCount(); i++){
	    javax.swing.text.Element childEl = el.getElement(i);
	    //System.err.println("Found child element " + childEl.getName());
	    if(HTML.Tag.A.equals(childEl.getName())){
		links.add(childEl);
	    }
	    else{
		for(int j = 0; j < childEl.getElementCount(); j++){
		    links.addAll(getHyperlinkElements(childEl.getElement(j)));
		}
	    }
	}
	return links;
    }

    /**
     * The search/filter widget is document specific (since you may want to have 
     * different view of different service responses), therefore setting the visibility is only
     * valid for the time the current document is being shown.
     */
    public void setFilterVisible(boolean visible) throws Exception{
	if(visible){
	    if(!filterHistory.containsKey(getCurrentURL())){
		if(deletedFilters.containsKey(getCurrentURL())){
		    // resurrect the previous filter for the doc that the user dismissed
		    filterHistory.put(getCurrentURL(), deletedFilters.get(getCurrentURL()));
		}
		else{
		    filterHistory.put(getCurrentURL(), new FilterSearch(getCurrentURL(), contentGUI.getDocumentBuilder()));
		}
	    }
	    logger.debug("Setting filter to "+filterHistory.get(getCurrentURL()) + " for " +getCurrentURL());
	    filterSearchWidget.setFilter(filterHistory.get(getCurrentURL()));
	    searchPanel.add(filterSearchWidget);
	    searchPanel.revalidate();
	    revalidate(); // redo the panel layout
	    applyFilter(true);
	}
	else{
	    // have a backup, so if re-enabled the old search is restored
	    if(filterHistory.containsKey(getCurrentURL())){
		deletedFilters.put(getCurrentURL(), filterHistory.remove(getCurrentURL()));
	    }
	    filterSearchWidget.setFilter(null);
	    if(canFilter()){applyFilter(false);}  // no filter GUI = no filter effect
	    searchPanel.remove(filterSearchWidget);
	    searchPanel.revalidate();
	    revalidate();
	}
    }

    /**
     * This method tries to figure out what kinds of MOBY Data could be 
     * created from the string given.  e.g. Keywords, DNASequence, etc.
     */
    protected MobyDataInstance[] loadMobyDataFromString(String data){
	return MobyUtils.convertStringToObjects(data);
    }

    public boolean isMobyURL(URL u){
	return u != null && (u.toString().indexOf(".xml#/") != -1 || 
                             u.toString().indexOf("http://moby/") != -1);
    }

    // MouseListener interface implementation
    public void mousePressed(MouseEvent e){
	// Allow middle button paste
	if(e.getButton() == MouseEvent.BUTTON2 ||
	   e.getButton() == MouseEvent.BUTTON3 && e.isAltDown()){
	    paste();
	}
    }

    /**
     * Grabs the keyboard focus for the component if the mouse is over it,
     * which eliminates the need for clicking before using keyboard shortcuts
     * like Control-v to paste.
     */
    public void mouseEntered(MouseEvent e){
	// There is one situation where we *don't* want to take the focus, when
	// the secondary parameter dialog has just been displayed.  We get a mouseEntered
	// callback when the popup menu used to select the service disappears,
	// and the secondary parameter dialog gets hidden behind the main window where
	// the user can't see it!
	if(servicesGUI.isSecondaryParamDialogNew()){
	    return;
	}

	if(!requestFocusInWindow()){
	    requestFocus();
	}
    }
    
    public void mouseExited(MouseEvent e){}
    
    public void mouseClicked(MouseEvent e){
	lastClickX = e.getX();
	lastClickY = e.getY();
	if(checkSelectionPressed(lastClickX, lastClickY)){
	    if(selectedTextData == null){
		// If there is no selected text, we need to reconstruct it from the info we have
		// about what was highlighted just before.
		if(oldMark != oldDot){
		    try{
			if(oldMark > oldDot){
			    selectedTextData = editorPane.getText(oldDot, oldMark-oldDot);
			}
			else{
			    selectedTextData = editorPane.getText(oldMark, oldDot-oldMark);
			}
		    } catch(javax.swing.text.BadLocationException ble){
			logger.warn("Bad dot/mark record for getText(): " + ble);
		    }
		    finally{
			oldMark = oldDot; // clear the old selection so only the first click activates the above code
		    }
		}
	    }
	    // todo: If the selection was over the Moby XML display, harvest the set of data and show options for those
	    // from experience, users try to highlight the whole data and send forward to the next service rather than
	    // just picking an example value.

	    // If display was not moby data links, but rather just text of some sort, do data recognition over it.
	    MobyDataInstance[] mobyData = loadMobyDataFromString(selectedTextData);
	    if(mobyData == null || mobyData.length == 0){
		logger.warn("No MobyData could be made from the selected text");
		return;
	    }

	    JPopupMenu popup = new DynamicJPopupMenu();
	    popup.setName(MOBY_SERVICE_POPUP_NAME);
	    popup.show(this, lastClickX+editorPane.getX(), lastClickY+editorPane.getY());
	    JMenuItem checkingLinksPopupItem = new JMenuItem("Checking for services...");
	    popup.add(checkingLinksPopupItem);
	    popup.setVisible(true);
	    // Things we could build automatically with the utilities
	    for(int i = 0; i < mobyData.length; i++){
		servicesGUI.addPopupOptions(mobyData[i], popup, true, getDefaultHandler()); // true = asynchronous
	    }

	    // Now for the things the data mapper find
	    servicesGUI.addPopupOptions(selectedTextData, popup, true, getDefaultHandler());

	    status.setText("Shift+Click=Service in new tab, Ctrl+Click=Use default 2nd params");
	    if(popup.getSubElements().length > 0){
		popup.setSelected(null);
		popup.setVisible(false);
		popup.remove(checkingLinksPopupItem);
		popup.setVisible(true);
	    }
	}
    }
    
    protected boolean checkSelectionPressed(int x, int y){
	int pressedLoc = editorPane.viewToModel(new Point(x, y));
	if(mark == dot){
	    if(oldMark < oldDot && (pressedLoc >= oldMark && pressedLoc <= oldDot) ||
	       oldMark > oldDot && (pressedLoc <= oldMark && pressedLoc >= oldDot)){
		// clicked on a spot that was just highlighted previously (but caret events happen before mouse events)
		return true;
	    }
	    else{
		oldMark = oldDot;
		return false;  // nothing selected
	    }
	}
	if(mark < dot && (pressedLoc >= mark && pressedLoc <= dot) ||
	   mark > dot && (pressedLoc <= mark && pressedLoc >= dot)){
	    return true;
	}
	else{
	    return false;
	}
    }
    
    public void mouseReleased(MouseEvent e){}
    
    public int print (Graphics g, PageFormat pf, int pageIndex) throws PrinterException{
	return editorPane.print(g, pf, pageIndex);
    }

    public PrintableJEditorPane getDisplay(){
	return editorPane;
    }

    public void caretUpdate(CaretEvent e) {
	//Get the location in the text
	dot = e.getDot();
	mark = e.getMark();
	if (dot == mark) {  // no selection
	    selectedTextData = null;
	    //status.setText("selection canceled");
	    return;
	}
	else{  // keep track of the previous selection so we can use it once it's gone
	    oldMark = mark;
	    oldDot = dot;
	}
	selectedTextData = editorPane.getSelectedText();
	status.setText("Click your selection to use it as MOBY data!");
    }

    /**
     * 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){
	if(e.getKeyCode() == KeyEvent.VK_PASTE ||
	    e.getKeyCode() == KeyEvent.VK_V && e.isControlDown()){
	    paste();
	}
	// Ctrl-T for new tab, like regular Web browsers
	if(e.getKeyCode() == KeyEvent.VK_T && e.isControlDown()){
	    MobyContentPane tab = contentGUI.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){}

    public void paste(){
	getTransferHandler().importData(this, Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this));
    }

    public void mouseDragged(MouseEvent e){
	// if a hyperlink is being dragged, and we're wrapping, this should be a copy event
	if(overHyperlink && !dragging){
	    dragging = true;  // so we don't export many times in a row while dragging
	    lastHyperlinkDragged = lastHyperlinkHovered;
	    status.setText("Drop the hyperlink onto a Web form field to populate it");
	    getTransferHandler().exportToClipboard(this, 
						   Toolkit.getDefaultToolkit().getSystemClipboard(), 
						   TransferHandler.COPY);
	    getTransferHandler().exportAsDrag(this, e, TransferHandler.COPY);
	}
	else if(!dragging){
	    //System.err.println("Dragging with no hyperlink activated");
	}
    }

    public void mouseMoved(MouseEvent e){
	// if a hyperlink is being dragged, and we're wrapping, this should be a copy event
	// normally it'd go to mouseDragged, but sometimes that's flakey so check manually for the 
	// mouse button being down here.
	if(overHyperlink && (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK){
	    //System.err.println("Got wonky drag");
	    lastHyperlinkDragged = lastHyperlinkHovered;
	    status.setText("Drop the hyperlink onto a Web form field to populate it");
	    //System.err.println("Dragging "+lastHyperlinkDragged);
	    getTransferHandler().exportAsDrag(this, e, TransferHandler.COPY);
	}
	else if(dragging){
	    dragging = false;
	}
    }

    public void lostOwnership(Clipboard clipboard, Transferable contents){
	//System.err.println("Lost clipboard ownership");
    }

}
