package ca.ucalgary.services.util;

import ca.ucalgary.services.WrappingServlet;
import ca.ucalgary.services.Registration;
import ca.ucalgary.seahawk.gui.*;
import ca.ucalgary.seahawk.services.*;
import ca.ucalgary.seahawk.util.SeahawkOptions;
import org.biomoby.client.CentralImpl;
import org.biomoby.service.MobyServlet;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;

import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;

import org.w3c.dom.*;

import javax.servlet.http.*;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import javax.xml.xpath.*;

import java.net.URL;
import java.util.*;
import java.util.logging.*;
import java.util.regex.*;
import java.io.*;

/**
 * This class controls the interaction between the web browser and Seahawk via the pasting actions.
 * The tracking of Moby data in a demonstration usage of a web service or html form is used
 * to generalize the invocation details into a Moby wrapper for the legacy service.
 */
public class PBERecorder implements DataRecorder{
    private Map<String,MobyContentGUI> sessionId2gui;  // link the http session with the GUI
    private Map<String,Node> sessionId2dom;  // link the http session with the results of the wrapped service
    private Map<String,String> sessionId2serviceSpec;  // link the http session with the wsdl service/port/op def or action/method/cgi def for CGIs
    private Map<String,String> sessionId2specLoc;  // link the http session with the wsdl or cgi doc URL
    private Map<String,String> sessionId2lastXPtr;  // link the http session with the ns resolution xptr context
    private Map<String,SourceMap> sessionId2SourceMap;  //the data posted to the web service
    private Map<String,Map<String,byte[]>> sessionId2InputParamsMap; //the data posted to the CGI
    private Map<String,List<String>> sessionId2HiddenParamsList; //the data posted to the CGI but not-user interactable
    private Map<String,MobyContentGUI> id2gui; // used when embedded in Seahawk for PBE, init id for session security
    private Map<String,Map<MobyDataInstance,String[]>> sessionId2PastedData;  //id -> <mobyobj -> [resultValue, transformURI]>
    private Map<HttpServletRequest,Map<String,String>> params; // shared internal temp params used while processing a request
    private Map<String,Map<String,String>> sessionId2FieldTransformMap; //field->ruleURI map for pasted data
    private Map<String,Map<String,MobyDataInstance>> sessionId2MobySrcs; //field->dragged Moby data map 
    private int lastChoice = -1; // GUI selection
    private String lastPastedSessionId = null; //use to unify the session receiving pasted data with the copied Seahawk data in PBE
    private MobyDataInstance lastCopiedSource = null; //see above
    private String lastCopiedValue = null; 
    private String lastCopiedRuleURI = null;
    private String statusMessage = "Fill in a working example usage of the form, by dragging data from Seahawk's clipboard into the form fields";

    private Transformer transformer;
    private Transformer nullTransformer;
    private static Logger logger = Logger.getLogger(PBERecorder.class.getName());
    private static LSIDResolver lsidResolver;

    private final static String DEFAULT_OUTPUT_NAME = "return"; // what the MobyService output is called
    //private static String PROXY_URL_BASE = "http://www.daggoo.net/";
    private static String PROXY_URL_BASE = "http://www.visualgenomics.ca:8765/";
    private final static String PROXY_REGISTER_URL_SUFFIX = "register";

    // output types form fields
    private final static String MOBY_OUTPUT_NS_PARAM = "mobyOutputNS";
    private final static String MOBY_OUTPUT_NS_FIELD_ID = "mobyOutputNSResolver";
    private final static String MOBY_OUTPUT_TYPE_PARAM = "mobyOutputType";
    private final static String MOBY_OUTPUT_TYPE_FIELD_ID = "mobyOutputTypeResolver";
    private final static String MOBY_OUTPUT_OP_PARAM = "mobyOutputOp";
    private final static String OUTPUT_RADIO_PARAM = "dataTypeChoice"; // to choose between a base object of something more complicated
    private final static String OUTPUT_RADIO_BASE = "base";
    private final static String OUTPUT_RADIO_SECONDARY = "secondary";
    private final static String OUTPUT_RADIO_OTHER = "other";

    // metadata form fields
    private final static String MOBY_SERVICETYPE_FIELD_ID = "mobyServiceTypeResolver";
    private final static String MOBY_CREATE_PARAM = "mobyServiceCreate";

    public final static String RESOURCE_PREFIX = "ca/ucalgary/services/resources/"; //location of auxillary files

    public static final String RESOURCE_FILE_PARAM = "_resource"; // cgi key to retrieve a file such as a CSS
    public static final String AUTOCOMPLETE_PARAM = "autocomplete";  // ajax param
    public static final String LIST_CHOICE_PARAM = "choice";  // ajax param
    public static final String RETURN_XPATH_PARAM = "ret"; // selection of nodes of interest
    public static final String VALUE_XPATH_PARAM = "val";  // scalars within selection to use in Moby obj construction
    public static final String CONTEXT_XPTR_PARAM = "contextXPtr";  // context for ns prefix:uri resolution
    public static final String OUTPUT_RULE_URI_PARAM = "outRuleURI"; // rule that transforms service schema to moby
    //public static final String INPUT_RULE_URI_PARAM = "inRuleURI"; // rule that transforms moby to service schema
    public static final String RAW_XPATH_PARAM = "rawNode"; // to see raw XML output
    public static final String XPATH_DEFAULT_NS_PREFIX = "t"; // needed otherwise xpaths don't resolve to correct ns in doc
    public static final String CUSTOM_VALUE_XPATH_OPTION = "Create a custom XPath";
    public static final String CUSTOM_INPUT_PART_OPTION = "Specify a list of possible values...";
    public static final String INT_INPUT_PART_OPTION = "Specify an integer range...";
    public static final String FLOAT_INPUT_PART_OPTION = "Specify a decimal number range...";
    public static final String DATE_INPUT_PART_OPTION = "Specify a date range...";
    public static final String CUSTOM_INPUT_PART_VALUES_SUFFIX = "_vals";
    public static final String SCALAR_INPUT_PART_MIN_SUFFIX = "_min";
    public static final String SCALAR_INPUT_PART_MAX_SUFFIX = "_max";
    private final static int ALL_MISSING_COUNT = 13; // how many fields will be blank when first submitted?

    // resources with this string in their contents get it substituted with the servlet's URL
    private static final String servletURLSubstitutionPattern = "%SERVLET_URL%"; 

    // HTML Instrumentation
    private static final String layerClass = "semitransparent";
    private static final String	cssText = "."+layerClass+" {\nfilter:alpha(opacity=85);\n"+
	"-moz-opacity:0.85;\nopacity: 0.85;\nz-index=1000}\n";// 'cause grayOut is z-index=50 and we want to be on top
    private static final String onLoadText = "if(navigator.product == 'Gecko'){firefoxOnPaste();}";
    private static final String layerId = "divStayCornerAnchored";
    private static final String divScriptText = "if (!document.layers){\n"+
	"  document.write('<div id=\""+layerId+"\" style=\"position:absolute; z-index=1000\">');\n}\n";
    private static final String layerScriptURL = "?"+RESOURCE_FILE_PARAM+"=floatingframe.js";
    private static final String inputEventAttributeName = "onpaste";
    private static final String inputEventAttributeValue = "pasteEvent(this)";
    private static final String submitEventAttributeName = "onsubmit";
    private static final String submitEventAttributeValue = "submit(this)";

    private static final String choiceEventAttributeName = "onclick";
    private static final String choiceEventSubstitutionSentinel = "INDEX";
    private static final String choiceEventAttributeValue = "choiceEvent("+choiceEventSubstitutionSentinel+"); return false";
    private static final String choiceScriptURL = "?"+RESOURCE_FILE_PARAM+"=list_choice.js";
    
    private static final String grayOutScriptURL = "?"+RESOURCE_FILE_PARAM+"=grayOut.js";

    private static Map<String,String[]> objUsageCache = null;
    private static HashMap<String,String[]> nsUsageCache = null;
    private static XPathFactory xPathFactory;
    private static DocumentBuilder docBuilder;

    static{
	xPathFactory = XPathFactory.newInstance();
	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        try{
            docBuilder = dbf.newDocumentBuilder();
        } catch(Exception e){
            e.printStackTrace();
        }
    }

    public PBERecorder(){
	sessionId2gui = new HashMap<String,MobyContentGUI>();
	sessionId2SourceMap = new HashMap<String,SourceMap>();
	sessionId2InputParamsMap = new HashMap<String,Map<String,byte[]>>();
	sessionId2HiddenParamsList = new HashMap<String,List<String>>();
	sessionId2dom = new HashMap<String,Node>();
	sessionId2lastXPtr = new HashMap<String,String>();
	sessionId2serviceSpec = new HashMap<String,String>();
	sessionId2specLoc = new HashMap<String,String>();
	sessionId2PastedData = new HashMap<String,Map<MobyDataInstance,String[]>>();
	sessionId2FieldTransformMap = new HashMap<String,Map<String,String>>();
	sessionId2MobySrcs = new HashMap<String,Map<String,MobyDataInstance>>();
	params = new HashMap<HttpServletRequest,Map<String,String>>();

	TransformerFactory transformerFactory = TransformerFactory.newInstance();
	try{
	    // verbatim copy 
	    nullTransformer = transformerFactory.newTransformer();  
	} catch (Exception e){
	    logger.log(Level.SEVERE,
		       "Could not create an XSLT transformer: " + e,
		       e);
	}
    }

    public void setGUIMap(Map<String,MobyContentGUI> map){
	id2gui = map;
    }

    public void startRecording(HttpServletRequest request){
	String id = request.getParameter(WrappingServlet.ID_PARAM);
	if(id != null && id2gui != null && id2gui.containsKey(id)){
	    MobyContentGUI gui = id2gui.get(id);
	    // create a session to track the form-filling instance
	    HttpSession session = request.getSession(true);
	    System.err.println("Setting GUI for session " + session.getId() + " to " + gui);
	    sessionId2gui.put(session.getId(), gui);
	}
	// Cache the object/namespace usage info that'll be used by AJAX autocomplete fields,
	// to improve response time
	(new Thread(){public void run(){getUsageInfo();}}).start();
    }

    /*
      
      Namespaces|Objects ontologyTerm #consumingService #producingServices mostConsumingHost1,mostConsumingHost2,... mostProducingHost1,mostProducingHost2,...
     */
    private synchronized void getUsageInfo(){
	if(objUsageCache != null){
	    return;
	}
	objUsageCache = new HashMap<String,String[]>();
	nsUsageCache = new HashMap<String,String[]>();
	try{
	    // todo: make this not gimpy hard-coded
	    URL url = new URL("http://www.visualgenomics.ca/gordonp/moby/service_counts.txt");
	    LineNumberReader reader = new LineNumberReader(new InputStreamReader(url.openStream()));
	    for(String line = reader.readLine(); line != null; line = reader.readLine()){
		String[] fields = line.split(" ", -10); 
		if(fields.length != 6){
		    System.err.println("Did not recognize format ("+fields.length+
				       " columns) of usage data file, line " + 
				       reader.getLineNumber()+": expected 6 columns");
		    continue;
		}
		
		String[] acceptors = fields[4].split(",");
		if(acceptors.length == 1 && acceptors[0].length() == 0){
		    acceptors = new String[]{};
		}
		String[] producers = fields[5].split(",");
		if(producers.length == 1 && producers[0].length() == 0){
		    producers = new String[]{};
		}
		
		if(fields[0].equals("Objects")){
		    objUsageCache.put(fields[1], new String[]{fields[2], fields[3], 
							      ""+acceptors.length, ""+producers.length, 
							      fields[4], fields[5]});
		}
		else if(fields[0].equals("Namespaces")){
		    nsUsageCache.put(fields[1], new String[]{fields[2], fields[3], 
							     ""+acceptors.length, ""+producers.length,
							     fields[4], fields[5]});
		}
		else{
		    System.err.println("Did not recognize first field ("+fields[0]+
				       ") of usage data file, line " + 
				       reader.getLineNumber()+": expected Objects or Namespaces");
		}
	    }
	} catch(Exception e){
	    e.printStackTrace();
	    logger.log(Level.SEVERE, "While loading remote object/namespace usage data", e);
	}
    }

    public void setTransformer(Transformer t){
	transformer = t;
    }

    public boolean shouldIntercept(HttpServletRequest request){
	return request.getParameter(PBERecorder.RETURN_XPATH_PARAM) != null ||
	    request.getParameter(PBERecorder.AUTOCOMPLETE_PARAM) != null ||
	    request.getParameter(PBERecorder.RESOURCE_FILE_PARAM) != null ||
	    request.getParameter(PBERecorder.RAW_XPATH_PARAM) != null;
	// TODO: deal with wsdl-less POST, which need writeWrapperForm call
    }

    public void interceptRequest(HttpServletRequest request,
				 HttpServletResponse response){

	String xpath = request.getParameter(PBERecorder.RETURN_XPATH_PARAM);
	String autocompleteRequest = request.getParameter(PBERecorder.AUTOCOMPLETE_PARAM);
	String resource = request.getParameter(PBERecorder.RESOURCE_FILE_PARAM);
	String raw = request.getParameter(PBERecorder.RAW_XPATH_PARAM);

	if(raw != null && raw.trim().length() != 0){
	    getRawXml(request, response);
	}
	else if(resource != null && resource.trim().length() != 0){
	    getResource(request, response);
	}
	//AJAX call for autocomplete
	else if(autocompleteRequest != null && autocompleteRequest.trim().length() != 0){
	    writeAutocompleteReply(request, response, autocompleteRequest);
	}
	else if(xpath != null && xpath.trim().length() != 0){
	    writeWrapperForm(request, response);
	}
	// TODO: deal with wsdl-less POST, which need writeWrapperForm call
	else{
	    // Shouldn't get here unless shouldIntercept() was not checked before calling this method, 
	    // or the logic of shouldIntercept() is inconsistent with that of this method.
	    
	}
    }

    private void writeAutocompleteReply(HttpServletRequest request,
					HttpServletResponse response,
					String seed){
	java.io.OutputStream out = null;
	response.setContentType("text/plain");
	try{
	    out = response.getOutputStream();
	}
	catch(java.io.IOException ioe){
	    logger.log(Level.SEVERE, "While getting servlet output stream (for HTML form response to client)", ioe);
	    return;
	}

	String answer = "";
	try{
	    answer = autocomplete(request, seed);
	} catch(Exception e){
	    //todo: make pretty, more verbose, etc., fit on one line for AJAX client
	    answer = e.toString();
	}

	try{
	    out.write(answer.getBytes());
	}
	catch(java.io.IOException ioe){
	    logger.log(Level.SEVERE, "While printing HTML form to servlet output stream", ioe);
	    return;
	}		
    }

    // This is called when the user has picked a part of the service response to return in the MOBY service being created
    private void writeWrapperForm(HttpServletRequest request,
				  HttpServletResponse response){
	java.io.OutputStream out = null;
	response.setContentType("text/html");
	try{
	    out = response.getOutputStream();
	}
	catch(java.io.IOException ioe){
	    logger.log(Level.SEVERE, "While getting servlet output stream (for HTML form response to client)", ioe);
	    return;
	}
	
	String answer = "";
	try{
	    out.write(writeWrapperForm(request).getBytes());
	    return;
	} catch(Exception e){
	    answer = "Error in PBE system:\n" + e.toString()+"\n";
	    for(StackTraceElement ste: e.getStackTrace()){
		answer += ste.toString()+"\n";
	    }
	}

	try{
	    out.write("<html><head><title>Service Definition Error</title></head>\n".getBytes());
	    out.write(("<body>"+answer+"</body></html>").getBytes());
	}
	catch(java.io.IOException ioe){
	    logger.log(Level.SEVERE, "While printing HTML form to servlet output stream", ioe);
	    return;
	}	
    }

    /**
     * Called when data in a field is being updated via a paste event.
     */
    public void doGet(HttpServletRequest request,
		      HttpServletResponse response){
	HttpSession session = request.getSession(true);
	MobyContentGUI gui = sessionId2gui.get(session.getId());

	// datatype<->event unification for Seahawk->browser drag 'n' drop
	String action = request.getParameter(DataRecorder.PASSTHROUGH_ACTION);
	//System.err.println("Event sent to PBE via AJAX is "+action);
	if(action.equals("pasteEvent")){
	    dataPasted(request);
	}
	else if(action.equals("choiceEvent")){
	    setChoice(request);
	}
	else{ //should be "hint"
	    getStatus(request, response);
	}
    }

    // Tell Seahawk that the WSDL dragged earlier is now available as the given service
    private void notifyWrappingCompletion(HttpServletRequest request,
					  String providerURI,
					  String serviceName,
					  MobyDataJob testCaseData) throws Exception{
	HttpSession session = request.getSession(true);
	MobyContentGUI gui = sessionId2gui.get(session.getId());
	gui.serviceWrapped(providerURI, serviceName, testCaseData);
    }

    public void getStatus(HttpServletRequest request,
			  HttpServletResponse response){
	synchronized(statusMessage){
	    response.setContentType("text/plain");
	    try{
		response.getOutputStream().write(statusMessage.getBytes());
	    } catch(Exception e){
		logger.log(Level.SEVERE, "Cannot write the response to a " + 
			   DataRecorder.PASSTHROUGH_ACTION + " request", e);
	    }
	}
    }

    /**
     * Show the user a set of choices in the browser window, 
     * and waits until they pick one before returning.  
     * Return -1 if canceled.
     */
    public int getChoice(String statusMsg, String[] choices){
	if(choices == null || choices.length == 0){
	    return -1;
	}
	synchronized(statusMessage){
	    StringBuilder sb = new StringBuilder();
	    sb.append("<table bgcolor='#EEEE00'><tr><td>"+statusMsg+"\n<ul>");
	    for(int i = 0; i < choices.length; i++){		
		sb.append("<li><a href=\"\" "+choiceEventAttributeName+"=\""+
			  choiceEventAttributeValue.replaceAll(choiceEventSubstitutionSentinel,""+i)+
			  "\">"+choices[i]+"</a></li>");
	    }
	    sb.append("</ul></td></tr></table>");
	    statusMessage = sb.toString();
	}

	// Wait for the user click
	int choice = -1;
	for(;;){
	    try{
		Thread.sleep(500);
	    } catch(Exception e){
		logger.log(Level.WARNING, "Got exception while sleeping between polls for choice response", e);
		break;
	    }
	    if(lastChoice != -1){		
		choice = lastChoice;
		lastChoice = -1;
		return choice;
	    }
	}
	return choice;
    }

    private void setChoice(HttpServletRequest request){
	String choiceString = request.getParameter(LIST_CHOICE_PARAM);
	int choice;
	//System.err.println("Set choice in PBERecorder to "+choiceString);
	try{
	    lastChoice = Integer.parseInt(choiceString);
	} catch(Exception e){
	    logger.log(Level.WARNING, "Could not parse integer response from "+choiceString, e);
	    lastChoice = -1; //error
	    return;
	}
	setStatus("Choice being applied...");
    }

    public void setStatus(String statusMsg){
	synchronized(statusMessage){
	    statusMessage = statusMsg;
	}
    }

    /**
     * Called when data a form is being submitted, or when submitting the PBE form
     */
    public void doPost(HttpServletRequest request,
		       HttpServletResponse response){
	HttpSession session = request.getSession(true);
	


	// Destroy related objects
// 	sessionId2gui.remove(session.getId());
// 	sessionId2fieldMap.remove(session.getId());
// 	sessionId2dom.remove(session.getId());
//	session.invalidate();
    }

    public String getOnEventText(){
	return " "+inputEventAttributeName+"=\""+inputEventAttributeValue+"\"";
    }

    public Attr getOnEventAsDOM(Document owner){
	Attr eventAttr = owner.createAttribute(inputEventAttributeName);
	eventAttr.setValue(inputEventAttributeValue);
	return eventAttr;
    }

    public String getOnSubmitText(){
	return " "+submitEventAttributeName+"=\""+submitEventAttributeValue+"\"";
    }

    public Attr getOnSubmitAsDOM(Document owner){
	Attr eventAttr = owner.createAttribute(submitEventAttributeName);
	eventAttr.setValue(submitEventAttributeValue);
	return eventAttr;
    }

    public String getHead(HttpServletRequest request){
	return "<STYLE TYPE=\"text/css\"> <!--\n" + cssText + "--></STYLE>\n";
    }

    public Element[] getHeadAsDOM(HttpServletRequest request, Document owner){
	Element style = owner.createElement("style");
	style.setAttribute("type", "text/css");
	style.appendChild(owner.createComment(cssText));
	return new Element[]{style};
    }

    public String getBodyAttrs(HttpServletRequest request){
	return " onLoad=\""+onLoadText+"\"";
    }

    public Attr[] getBodyAttrsAsDOM(HttpServletRequest request, Document owner){
	Attr onLoad = owner.createAttribute("onLoad");
	onLoad.setValue(onLoadText);
	return new Attr[]{onLoad};
    }

    public String getBody(HttpServletRequest request){
	// Creates a floating frame
	return "<script src=\""+request.getRequestURL()+choiceScriptURL+"\" type=\"text/javascript\"></script>\n"+
	    "<script>\n"+divScriptText+"</script>\n\n"+
	    "<layer id=\""+layerId+" class=\""+layerClass +"\"></layer>\n\n"+
	    "<script src=\""+request.getRequestURL()+layerScriptURL+"\" type=\"text/javascript\"></script>\n";
    }

    public Element[] getBodyAsDOM(HttpServletRequest request, Document owner){
	Element[] bodyNodes = new Element[4];
	bodyNodes[0] = owner.createElement("script");
	bodyNodes[0].appendChild(owner.createTextNode(divScriptText));
	bodyNodes[1] = owner.createElement("layer");
	bodyNodes[1].setAttribute("id", layerId);
	bodyNodes[1].setAttribute("class", layerClass);
	bodyNodes[2] = owner.createElement("script");
	bodyNodes[2].setAttribute("src", request.getRequestURL()+layerScriptURL);
	bodyNodes[2].setAttribute("type", "text/javascript");
	// the following line is needed because browsers don't like empty script tags
	bodyNodes[2].appendChild(owner.createTextNode(" "));

	bodyNodes[3] = owner.createElement("script");
	bodyNodes[3].setAttribute("src", request.getRequestURL()+choiceScriptURL);
	bodyNodes[3].setAttribute("type", "text/javascript");
	bodyNodes[3].appendChild(owner.createTextNode(" "));
	
	return bodyNodes;
    }

    public byte[] markupResponse(byte[] responseBody, String contentType, String charSet, 
				 HttpServletRequest request) throws Exception{
	// Keep the DOM, plus the specs of the WSDL service for use later in the wrapping process
	HttpSession session = request.getSession(false);
	if(session == null){
	    throw new Exception("Cannot find an HTTP session associated with this request");
	}
	// For now, these don't do anything, as the caller, CGIServlet, uses multipart encoded forms
	// Keep these calls here so this'll work if CGIServlet changes the way it does things.
	setParameter(session, WrappingServlet.SERVICE_SPEC_PARAM, request.getParameter(WrappingServlet.SERVICE_SPEC_PARAM));
	setParameter(session, WrappingServlet.SRC_PARAM, request.getParameter(WrappingServlet.SRC_PARAM));

	// share the results with the contentGUI
	MobyContentGUI gui = sessionId2gui.get(session.getId());
	if(gui == null){
	    throw new Exception("Cannot find a Seahawk GUI associated with the HTTP session (" + session.getId() +")");
	}
	// this'll apply MOB rules to see if anything recognizable as Moby data is in the response
	// It'll also assign xpaths in a seahawk namespaced attr to 
	int numObjects = gui.processCGIResults(responseBody, contentType);  

	// throw a grey cloak in front of HTML results, telling the user to select the Moby output
	if(contentType.indexOf("html") != -1){
	    sessionId2dom.put(session.getId(), MobyContentGUI.convertHTMLToXHTML(responseBody));

	    setStatus("Click your desired output in the Seahawk window in order to create a service");
	    String grayOutScript = "<script src=\""+request.getRequestURL()+grayOutScriptURL+
		"\" type=\"text/javascript\"></script>\n";
	    String bodyContents = new String(responseBody, charSet);
	    responseBody = bodyContents.replaceFirst("<head>", "<head>"+grayOutScript)
		                       .replaceFirst("<body(.*?)>", "<body onLoad=\"grayOut(true)\" $1>"+getBody(request)).getBytes();
	}
	else if(contentType.indexOf("xml") != -1){
	    sessionId2dom.put(session.getId(), docBuilder.parse(new ByteArrayInputStream(responseBody)));
	}

	clearInternalParams(request);

	return responseBody;
    }

    /**
     * Manipulates the Web Service response data so that it includes a bunch of links that 
     * can be used to create Moby services.  Also notes the WSDL location for future reference.
     */
    public String markupResponse(Source resultSource, HttpServletRequest request) throws Exception{

	if(transformer == null){
	    transformer = TransformerFactory.newInstance().newTransformer();
	}

	// Turn the response into a DOM we can navigate for juicy data bits
	Node domNode = null;
	if(resultSource instanceof DOMSource){
	    domNode = ((DOMSource) resultSource).getNode();
	}
	else{
	    // or make the DOM if non-existent
	    DOMResult domResult = new DOMResult();
	    try{
		synchronized(nullTransformer){
		    nullTransformer.transform(resultSource, domResult);
		}
	    } catch(Exception e){
		throw new Exception(e.getClass().getName() + " while transforming response from " +
				    "the service (probably an internal error): " + e);
	    }
	    domNode = domResult.getNode();
	}

	if(domNode == null){
	    throw new Exception("Could not get the result node from the result " +
				"DOM (bad null transformation?)");
     	}

	// Keep the DOM, plus the specs of the WSDL service for use later in the wrapping process
	HttpSession session = request.getSession(false);
	if(session == null){
	    throw new Exception("Cannot find an HTTP session associated with this request");
	}
	sessionId2dom.put(session.getId(), ((Document) domNode).cloneNode(true));
	setParameter(session, WrappingServlet.SERVICE_SPEC_PARAM, request.getParameter(WrappingServlet.SERVICE_SPEC_PARAM));
	setParameter(session, WrappingServlet.SRC_PARAM, request.getParameter(WrappingServlet.SRC_PARAM));

	// share the results with the contentGUI
	MobyContentGUI gui = sessionId2gui.get(session.getId());
	if(gui == null){
	    throw new Exception("Cannot find a Seahawk GUI associated with the HTTP session (" + session.getId() +")");
	}
	// this'll apply MOB rules to see if anything recognizable as Moby data is in the response
	// It'll also assign xpaths in a seahawk namespaced attr to 
	int numObjects = gui.processServiceResults(domNode);  

	// Todo: reassign namespaces if necessary so no prefixes (including the default)
	// get clobbered.  This solves the problem of XPath resolution, where the namespace context
	// cannot be easily defined except relative to the example node used.
	
	ByteArrayOutputStream stringResult = new ByteArrayOutputStream();
	try{
	    synchronized(transformer){
		transformer.transform(new DOMSource(domNode), 
				      new javax.xml.transform.stream.StreamResult(stringResult));
	    }
	} catch(Exception e){
	    throw new Exception(e.getClass().getName() + " while transforming response from " +
				"the service (probably an internal error): " + e);	    
	}

	//System.err.println(stringResult.toString());
	// Replace any enclosing tag with links that indicate the tag contents are of 
	// interest based on tag or attribute name, or attribute value
	String markup = stringResult.toString().replaceAll("</", "&lt;/");
	Pattern elementPattern = Pattern.compile("<(\\S+).*?>", Pattern.MULTILINE | Pattern.DOTALL);
	Pattern xpathPattern = Pattern.compile(MobyContentGUI.SEAHAWK_NS_PREFIX+":"+MobyContentGUI.SEAHAWK_XPATH_ATTR+
					       "=\"(.*?)#(.*?)\"", Pattern.MULTILINE | Pattern.DOTALL);
	Pattern attributePattern = Pattern.compile("(\\S+)=\"([^\"]*)\"", Pattern.MULTILINE | Pattern.DOTALL);

	StringBuilder newMarkup = new StringBuilder();
	Matcher elementMatcher = elementPattern.matcher(markup);
	int markupLengthRemoved = 0;
	while(elementMatcher.find()){
	    // These data structure keeps track changes we need to make to elementText
	    Vector<int[]> ranges = new Vector<int[]>();
	    Vector<String> replacements = new Vector<String>();

	    // In addition to adding the links below, replace the opening '<' with '&lt;' so the user
	    // sees the XML structure properly in the browser
	    addEdit(ranges, new int[]{0,1}, replacements, "&lt;");

	    String elementText = elementMatcher.group(0);
	    String elementName = elementMatcher.group(1);

	    Matcher xpathMatcher = xpathPattern.matcher(elementText);
	    if(xpathMatcher.find()){
		String returnXPath = xpathMatcher.group(1);
		String contextXPtr = xpathMatcher.group(2);
		// link = element name is the criterion, stuff to capture is assumed to be text contents of tag
		String elementLink = buildSelectionLink(returnXPath, contextXPtr, "text()", elementName); 
		addEdit(ranges, 
			new int[]{elementMatcher.start(1)-elementMatcher.start(), elementMatcher.end(1)-elementMatcher.start()}, 
			replacements, elementLink);

		Matcher attributeMatcher = attributePattern.matcher(elementText);
		while(attributeMatcher.find()){
		    if(attributeMatcher.group(0).equals(xpathMatcher.group(0))){
			addEdit(ranges, new int[]{xpathMatcher.start(), xpathMatcher.end()}, 
				replacements, "");
			continue; //mark up any attribute except the Seahawk XPath one, which is deleted
		    }
		    String attrName = attributeMatcher.group(1);
		    String attrValue = attributeMatcher.group(2);

		    if(attrValue.equals(MobyContentGUI.SEAHAWK_NS_URI)){
			addEdit(ranges, new int[]{attributeMatcher.start(), attributeMatcher.end()}, 
				replacements, "");
			continue;
			// get rid of any seahawk-specific xmlns declarations too
		    }
		    // sometimes a literal document has encoded attributes: get rid of them
		    if(isLiteral(request) && attrName.equals("xsi:type")){
			addEdit(ranges, new int[]{attributeMatcher.start(), attributeMatcher.end()}, 
				replacements, "");
			continue;
		    }

		    // link = element name + attribute presence are the criteria, attribute value assumed to be capture
		    String attrNameLink = buildSelectionLink(returnXPath+"[@"+attrName+"]", contextXPtr, "@"+attrName, attrName);
		    addEdit(ranges, new int[]{attributeMatcher.start(1), attributeMatcher.end(1)}, 
			    replacements, attrNameLink);

		    // link = element name + attribute name&value are the criteria, text contents is to be captured
		    String attrValueLink = buildSelectionLink(returnXPath+"[@"+attrName+"=\""+attrValue+"\"]", 
							      contextXPtr, "text()", attrValue);
		    addEdit(ranges, new int[]{attributeMatcher.start(2), attributeMatcher.end(2)}, 
			    replacements, attrValueLink);
		}
		// literal string replacement
		String linkedElement = applyEdits(elementText, ranges, replacements);
		newMarkup.append(markup.substring(markupLengthRemoved, elementMatcher.start())); 
		newMarkup.append(linkedElement);
		markupLengthRemoved = elementMatcher.end();
	    }
	}

	clearInternalParams(request);

	return "\n"+"<table border=\"1\"><tr bgcolor=\"#DDDDFF\"><td>"+
	    "Below, click the most indented <b>element name</b>, <b>attribute name</b>, or <b>specific attribute value</b> "+
	    "that you expect to be <b>always present</b> in a <b>successful</b> execution. You will then be taken to "+
	    "a form asking you about how you want this data returned using the Moby data types." +
	    "</td></tr></table>"+"<pre>"+newMarkup+"</pre>";
    }

    private void addEdit(Vector<int[]> exciseRanges, int[] newRange, 
			 Vector<String> replacementStrings, String newReplacement){
	exciseRanges.add(newRange);
	replacementStrings.add(newReplacement);
    }

    // Apply all the edits from the last to first (so the excise ranges remain correct)
    // We assume the edits are non-overlapping, otherwise this'll fail miserably
    private String applyEdits(String src, Vector<int[]> exciseRanges, Vector<String> replacementStrings){
	String edited = src;
	for(int i = exciseRanges.size()-1; i >= 0; i--){
	    int[] remove = exciseRanges.elementAt(i);
	    edited = edited.substring(0, remove[0]) + replacementStrings.elementAt(i) + 
		(remove[1] >= edited.length() ? "" : edited.substring(remove[1]));
	}
	return edited;
    }

    private String buildSelectionLink(String returnXPath, String contextXPtr, String valXPath, String anchorText){
	return "<a href=\"?"+RETURN_XPATH_PARAM+"="+escape(returnXPath)+"&"+
	    CONTEXT_XPTR_PARAM+"="+escape(contextXPtr)+//"&"+VALUE_XPATH_PARAM+"="+escape(valXPath)+
	    "\">"+anchorText+"</a>";
    }

    private String escape(String text){
	return text.replaceAll("&", "&amp;").replaceAll(" ", "%20").replaceAll("\"", "&quot;");
    }

    /**
     * Called via AJAX when the user is filling something on the form that's supposed to be the Moby ontology
     * The returned value is the list of possible ontology matches, with relevant description excerpts.
     *
     * @param seed a string of the form datatype:xxx or namespace:xxx or servicetype:xxx or just xxx to search all
     */
    public String autocomplete(HttpServletRequest request, String seed){
	StringBuilder nameResults = new StringBuilder();
	StringBuilder descResults = new StringBuilder();

	HttpSession session = request.getSession(true);
	MobyContentGUI gui = sessionId2gui.get(session.getId());

	String[] seedparts = seed.split(":");
	
	boolean all = false;
	if(seedparts.length == 1){
	    all = true;
	}
	String search = seedparts[1].toLowerCase();
	int numResults = 0;
	try{
	    if(all || seedparts[0].equals("datatype")){
		System.err.println("Trying to find '" + search + "' in data types");
		for(MobyDataType datatype: gui.getMobyCentralImpl().getDataTypes()){		    
		    if(datatype.getName().toLowerCase().indexOf(search) != -1){
			nameResults.append(datatype.getName()+" - "+getContext(datatype.getDescription(), "", 50)+
					   getPopularity(datatype)+"\n");
		    }
		    else if(datatype.getDescription().toLowerCase().indexOf(search) != -1){
			descResults.append(datatype.getName()+" - "+getContext(datatype.getDescription(), search, 50)+
					   getPopularity(datatype)+"\n");
		    }
		}
	    }
	    if(all || seedparts[0].equals("namespace")){
		System.err.println("Trying to find '" + search + "' in namespaces");
		for(MobyNamespace namespace: gui.getMobyCentralImpl().getFullNamespaces()){
		    if(namespace.getName().toLowerCase().indexOf(search) != -1){
			nameResults.append(namespace.getName()+" - "+getContext(namespace.getDescription(), "", 50)+
					   getPopularity(namespace)+"\n");
		    }
		    else if(namespace.getDescription().toLowerCase().indexOf(search) != -1){
			descResults.append(namespace.getName()+" - "+getContext(namespace.getDescription(), search, 50)+
					   getPopularity(namespace)+"\n");
		    }
		}
	    }
	    if(all || seedparts[0].equals("servicetype")){
		System.err.println("Trying to find '" + search + "' in servicetypes");
		for(Map.Entry<String,String> servicetype: gui.getMobyCentralImpl().getServiceTypes().entrySet()){
		    if(servicetype.getKey().toLowerCase().indexOf(search) != -1){
			nameResults.append(servicetype.getKey()+" - "+getContext(servicetype.getValue(), "", 50)+
					   " " + getLineage(servicetype.getKey())+"\n");
		    }
		    else if(servicetype.getValue().toLowerCase().indexOf(search) != -1){
			descResults.append(servicetype.getKey()+" - "+getContext(servicetype.getValue(), search, 50)+
					   " " + getLineage(servicetype.getKey())+"\n");
		    }
		}
	    }
	} catch(Exception e){
	    descResults.append("[Exception - cannot autocomplete: " + e + "]");
	}
	return nameResults.toString() + descResults.toString();
    }

    // resultCount used to determine how much time we should devote to retrieving popularity data,
    // in order to keep the response interactive
    private String getPopularity(Object o){

	if(o instanceof MobyDataType){
	    String[] stats = objUsageCache.get(((MobyDataType) o).getName());
	    if(stats == null){return "[usage stats: n/a]";}
	    return " [services: "+stats[0]+" "+getHostInfo("accept",stats[4],stats[2])+", "+
		stats[1]+" "+getHostInfo("produce",stats[5],stats[3])+"]";
	}
	else if(o instanceof MobyNamespace){
	    String[] stats = nsUsageCache.get(((MobyNamespace) o).getName());
	    if(stats == null){return "[usage stats: none]";}
	    return " [services: "+stats[0]+" "+getHostInfo("accept",stats[4],stats[2])+", "+
		stats[1]+" "+getHostInfo("produce",stats[5],stats[3])+"]";
	}

	return "";	
    }

    private String getHostInfo(String type, String list, String total){
	return "<acronym title=\""+(type.equals("produce")?"Producers":"Consumers")+"("+total+"): "+
	    list.replaceAll(","," ")+"\">"+type+"</acronym>";
    }

    // give surrounding words (numChars worth total) around targetWord in text
    private String getContext(String text, String targetWord, int numChars){
	return getContext(text, targetWord, numChars, true); // true = use tool tips for ellipsis expansion
    }

    private String getContext(String text, String targetWord, int numChars, boolean useToolTip){
	text = text.replaceAll("\n",""); //new-line obvious messes up ajax one-line per response format
	
	if(text.length() < numChars){
	    return text;  //can fit the whole thing
	}
	
	int start = 0;
	if(targetWord.length() > 0){
	    start = text.toLowerCase().indexOf(targetWord)-numChars/2;
	}
	if(start < 0){
	    numChars -= start;
	}

	int end = text.length();
	if(text.indexOf(" ") == -1){
	    end = numChars;
	}
	else if(start + targetWord.length() + numChars < text.length()){
	    end = start + targetWord.length() + numChars;
	    // break at a word boundary
	    while(end < text.length() && !Character.isWhitespace(text.charAt(end))){
		end++;
	    }
	}

	if(start <= 0){
	    start = 0;
	}
	else{
	    // break at a word boundary
	    while(start > 0 && !Character.isWhitespace(text.charAt(start))){
		start--;
	    }
	}
	
	String prefix = start == 0 ? "" : (useToolTip ? "<acronym title=\""+text.substring(0,start)+"\">...</acronym>" : "...");
	String suffix = end == text.length() ? "" : (useToolTip ? "<acronym title=\""+text.substring(end)+"\">...</acronym>" : "...");

	return prefix+text.substring(start, end)+suffix;
    }

    public void getRawXml(HttpServletRequest request, HttpServletResponse response){
	try{
	    java.io.OutputStream out = response.getOutputStream();

	    HttpSession session = request.getSession(true);
	    Node domNode = sessionId2dom.get(session.getId());

	    String xPath = getParam(request, RAW_XPATH_PARAM);
	    NodeList rawNodes = null;
	    XPath xPathObj = xPathFactory.newXPath();
	    try{
		xPathObj.setNamespaceContext(getNsContext(request));
		rawNodes = (NodeList) xPathObj.evaluate(fixXPath(xPath, request), domNode, XPathConstants.NODESET);
	    } catch(Exception e){
		String err = e.toString()+"\n";
		for(StackTraceElement ste: e.getStackTrace()){
		    err += ste.toString()+"\n";
		}
		response.setContentType("text/plain");
		out.write(err.getBytes());
		return;
	    }

	    if(rawNodes.getLength() == 0){
		response.setContentType("text/plain");
		out.write(("The XPath " + xPath + " did not select any nodes").getBytes());
	    }
	    else if(rawNodes.getLength() != 1){
		response.setContentType("text/html");
		out.write(("<html><body>The XPath " + xPath + " selected " + rawNodes.getLength() + "  nodes. ").getBytes());
		out.write("Select from the results below:<br/><br/>".getBytes());
		for(int i = 0; i < rawNodes.getLength(); i++){
		    out.write(("<a href='?"+RAW_XPATH_PARAM+"="+xPath+"["+(i+1)+"]'>"+
			       rawNodes.item(i).getNodeName()+"</a><br/>\n").getBytes());
		}
		out.write("</body></html>".getBytes());
	    }
	    else{
		StringWriter stringWriter = new StringWriter();
		StreamResult streamResult = new StreamResult(stringWriter);
		try{
		    synchronized(nullTransformer){  //just serializes
			nullTransformer.transform(new DOMSource(rawNodes.item(0)), streamResult);
		    }
		} catch(Exception e){
		    out.write((e.getClass().getName() + " while transforming response from " +
			       "the service (probably an internal error): ").getBytes());
		    String err = e.toString()+"\n";
		    for(StackTraceElement ste: e.getStackTrace()){
			err += ste.toString()+"\n";
		    }
		    response.setContentType("text/plain");
		    out.write(err.getBytes());
		    return;
		}
		response.setContentType("text/xml");  //assume text-based
		out.write(stringWriter.toString().getBytes());
	    }
	}
	catch(java.io.IOException ioe){
	    logger.log(Level.SEVERE, "While getting XPath results", ioe);
	}
    }

    // retrieves auxillary text files use in the course of wrapping, such as .js and .css
    public void getResource(HttpServletRequest request, HttpServletResponse response){

	String resourceName = request.getParameter(RESOURCE_FILE_PARAM);

	try{
	    URL u = getClass().getClassLoader().getResource(RESOURCE_PREFIX+resourceName);
	    InputStream rez = null;
	    if(u != null){
		rez = u.openStream();
	    }
	    else{
		response.setStatus(HttpServletResponse.SC_NOT_FOUND); 
	    }
	    
	    response.setContentType("text/plain");  //assume text-based
	    java.io.OutputStream out = response.getOutputStream();

	    String servletURL = request.getRequestURL().toString();
	    if(rez != null){
		LineNumberReader lnr = new LineNumberReader(new InputStreamReader(rez));
		String line = null;
		while((line = lnr.readLine()) != null){
		    out.write((line.replaceAll(servletURLSubstitutionPattern, servletURL)+"\n").getBytes());
		}
	    }
	    else{
		out.write(("The resource " + RESOURCE_PREFIX+resourceName + " could not be found by the servlet").getBytes());
	    }
	}
	catch(java.io.IOException ioe){
	    logger.log(Level.SEVERE, "While getting resource (for PBE HTML form response to client)", ioe);
	}
    }

    /**
     * Uses the retained copy of the WSDL service response, matching it up with the xPath given by the user,
     * to create an interface confirming the definition of a new Moby service.
     */
    public String writeWrapperForm(HttpServletRequest request){
	StringBuilder markup = new StringBuilder();

	// See if all the fields have been filled in correctly, and the user submitted the form
	Map<String,String> todoText = validateForm(request);

	if(todoText.size() == 0){
	    // Actually launch the service
	    markup.append(wrappingServiceCreation(request));
	}
	else{
	    // Requirements gathering form
	    markup.append(wrappingHeader(request, todoText));
	    markup.append(wrappingOutputSelection(request, todoText));
	    markup.append(wrappingOutputType(request, todoText));
	    markup.append(wrappingInputTypes(request, todoText));
	    markup.append(wrappingMetadata(request, todoText));
	    markup.append(wrappingFooter(request, todoText));
	}

	clearInternalParams(request);

	// If the result is from a service that didn't take any Moby objects or secondaries as input, 
	// we can use the result as input to another service in the WSDL.  This is often useful where
	// a service requires as a parameter something from within its own schema.  e.g. service A 
	// returns the list of database targets available, and service B takes two parameters, a target database
	// and a query string.  Linking the output of service A to create a selection list for service
	// B makes the wrapping process more generically useful for future users of the Moby service

	return markup.toString();
    }

    // If everything's filled in correctly, the map is blank, otherwise it's filled with
    // error messages
    private Map<String,String> validateForm(HttpServletRequest request){
	Map<String,String> todo = new HashMap<String,String>();
	String val = null;
	
	HttpSession session = request.getSession(true);
	MobyContentGUI gui = sessionId2gui.get(session.getId());

	//******Output types section******
	val = request.getParameter(OUTPUT_RADIO_PARAM); 
	if((val == null || val.trim().length() == 0)){	    
	    if(request.getParameter(OUTPUT_RULE_URI_PARAM) == null ||
	       request.getParameter(OUTPUT_RULE_URI_PARAM).trim().length() == 0){
		todo.put(MOBY_OUTPUT_NS_PARAM, "One of the output type radio buttons in the left column must be selected");
	    }
	}
	else{
	    if(val.equals(OUTPUT_RADIO_BASE)){
		val = request.getParameter(MOBY_OUTPUT_NS_PARAM);
		if(val == null || val.trim().length() == 0){
		    todo.put(MOBY_OUTPUT_NS_PARAM, "A namespace-based return value was selected at left, " +
			     "please use the autocomplete to select a particular namespace.");
		}
		else{
		    val = val.trim();
		    if(val.indexOf(' ') != -1){
			val = val.substring(0, val.indexOf(' '));// only the first word is used, the rest must be description
		    }
		    if(MobyNamespace.getNamespace(val, SeahawkOptions.getRegistry()) == null){
			todo.put(MOBY_OUTPUT_NS_PARAM, "No Moby namespace called '"+val+
				 "' was found, please select from the autocompletion list");
		    }
		}
	    }
	    else if(val.equals(OUTPUT_RADIO_OTHER)){
		val = request.getParameter(MOBY_OUTPUT_TYPE_PARAM);
		if(val == null || val.trim().length() == 0){
		    todo.put(MOBY_OUTPUT_TYPE_PARAM, "A datatype-based return value was selected at left, " +
			     "please use the autocomplete to select a particular datatype.");
		}
		else{
		    val = val.trim();
		    if(val.indexOf(' ') != -1){
			val = val.substring(0, val.indexOf(' '));// only the first word is used, the rest must be description
		    }
		    if(MobyDataType.getDataType(val, SeahawkOptions.getRegistry()) == null){
			todo.put(MOBY_OUTPUT_TYPE_PARAM, "No Moby datatype called '"+val+
				 "' was found, please select from the autocompletion list");
		    }
		}	    
	    }
	    else if(val.equals(OUTPUT_RADIO_SECONDARY)){
		val = request.getParameter(MOBY_OUTPUT_OP_PARAM);
		if(val == null || val.trim().length() == 0){
		    todo.put(MOBY_OUTPUT_TYPE_PARAM, "The operation-input mode was selected at left, " +
			     "please select an operation to pipe results to.");
		}
	    }
	    else{
		todo.put(MOBY_OUTPUT_NS_PARAM, "The value of parameter '"+OUTPUT_RADIO_PARAM+"' is not recognized");
	    }
	}

	// ******Metadata section******
        // direct passthrough to registration servlet (Daggoo)					       
	val = request.getParameter(Registration.MOBY_SERVICENAME_PARAM);
	if(val == null){
	    todo.put(Registration.MOBY_SERVICENAME_PARAM, "Cannot be left blank. "+
		     "Please be descriptive, this is the name other Moby users will see.");
	}
	else if(!val.matches("[A-Za-z0-9]+")){
	    todo.put(Registration.MOBY_SERVICENAME_PARAM, 
		     "Contains non-alphanumeric characters, please correct");
	}

	val = request.getParameter(Registration.MOBY_SERVICETYPE_PARAM);
	if(val == null){
	    todo.put(Registration.MOBY_SERVICETYPE_PARAM, 
		     "Cannot be left blank, please type a keyword for suggestions to appear");
	}
	else{
	    val = val.trim();
	    if(val.indexOf(' ') != -1){
		val = val.substring(0, val.indexOf(' '));// only the first word is used, the rest must be description
	    }

	    if(MobyServiceType.getServiceType(val, SeahawkOptions.getRegistry()) == null){
		todo.put(Registration.MOBY_SERVICENAME_PARAM, 
			 "No Moby service type with the name '"+val+
			 "' was found, please select from the autocompletion list");
	    }
	}

	val = request.getParameter(Registration.MOBY_AUTHOR_PARAM);
	if(val == null){
	    todo.put(Registration.MOBY_AUTHOR_PARAM, "The e-mail cannot be left blank.");
	}
	else{
	    val = val.trim();
	    if(!val.matches("\\S+@[0-9a-zA-Z\\-\\.]+\\.[a-zA-Z]+")){
		todo.put(Registration.MOBY_AUTHOR_PARAM, 
			 "The value doesn't appear to have the form of an e-mail "+
			 "address.  Please correct it.");
	    }
	}
	
	val = request.getParameter(Registration.MOBY_DESC_PARAM);
	if(val == null){
	    todo.put(Registration.MOBY_DESC_PARAM, "The description cannot be left blank");
	}
	else{
	    val = val.trim();
	    if(val.length() < 50){
		todo.put(Registration.MOBY_DESC_PARAM, 
			 "The functional description is not descriptive enough!  "+
			 "Please use at least 50 characters.");
	    }
	}

	return todo;
    }

    public static String getProxyBaseUrl(){
	return PROXY_URL_BASE;
    }

    // Everything's filled in, we need to create the semantically annotated service now...
    // map the info we have to the fields regquired by the registration servlet (Registration)
    // that'll persist this info for service creation.
    private String wrappingServiceCreation(HttpServletRequest request){
	HttpSession session = request.getSession(false);

	PostMethod registrationRequest = new PostMethod(PROXY_URL_BASE+PROXY_REGISTER_URL_SUFFIX);

	String servicePortOpRootEncoding = sessionId2serviceSpec.get(session.getId());
	registrationRequest.setParameter(Registration.OP_PARAM, servicePortOpRootEncoding);

	String specDocURL = sessionId2specLoc.get(session.getId());
	registrationRequest.setParameter(Registration.OP_SPEC_DOC_PARAM, specDocURL);
	
	// Passthrough (may be blank)
	registrationRequest.setParameter(Registration.MOBY_AUTHOR_PARAM, 
					 request.getParameter(Registration.MOBY_AUTHOR_PARAM));

	// Passthrough
	registrationRequest.setParameter(Registration.MOBY_SERVICENAME_PARAM,
					 request.getParameter(Registration.MOBY_SERVICENAME_PARAM));

	registrationRequest.setParameter(Registration.MOBY_DESC_PARAM,
					 request.getParameter(Registration.MOBY_DESC_PARAM));

	String serviceType = cleanTerm(request.getParameter(Registration.MOBY_SERVICETYPE_PARAM));
	if(!serviceType.startsWith("urn:lsid")){
	    MobyServiceType st = MobyServiceType.getServiceType(serviceType, SeahawkOptions.getRegistry());
	    if(st == null){
		return "Couldn't find the service type "+serviceType+ 
		    " in the registry, please go back and correct it";
	    }
	    serviceType = st.getLSID();
	}
	registrationRequest.setParameter(Registration.MOBY_SERVICETYPE_PARAM, serviceType);

	org.biomoby.registry.meta.Registry registry = SeahawkOptions.getRegistry();
	if(registry == null){
	    registry = org.biomoby.registry.meta.RegistryCache.getDefaultRegistry();
	}
	String registryEndpointURL = registry.getEndpoint();
	registrationRequest.setParameter(Registration.MOBY_CENTRAL_PARAM, registryEndpointURL);

	StringBuilder inputsSpec = new StringBuilder();	
	StringBuilder inURISpec = new StringBuilder();	//transform rule URIs
	StringBuilder secondarySpec = new StringBuilder();
	StringBuilder secURISpec = new StringBuilder();
	StringBuilder fixedSpec = new StringBuilder();
	Map<String,String> namesInUse = new HashMap<String,String>();
	MobyDataJob testCaseData = new MobyDataJob();  // to record sample data
	if(isWSDL(request)){
	    SourceMap fields = sessionId2SourceMap.get(session.getId());
	    Document inDoc = null;
	    try{
		inDoc = docBuilder.parse(new StringBufferInputStream(fields.toString()));
		NodeList elements = inDoc.getDocumentElement().getElementsByTagName("*");  //all subelements
		for(int i = 0; i < elements.getLength(); i++){
		    Element element = (Element) elements.item(i);
		    String fieldName = getInputFieldName(element);
		    String textContent = element.getTextContent();
		    if(textContent == null || textContent.trim().length() == 0){
			continue;
		    }
		    // a value node, not just a container: find the input parameters that correspond to it
		    addField(fieldName, textContent, testCaseData, request, namesInUse,
			     inputsSpec, inURISpec, secondarySpec, secURISpec, fixedSpec);
		}
	    } catch(IllegalArgumentException iae){
		return iae.getMessage();
	    } catch(Exception e){
		String err = e.toString()+"\n";
		for(StackTraceElement ste: e.getStackTrace()){
		    err += ste.toString()+"\n";
		}
		return "[<font color=\"red\">Error parsing input data XML: <pre>"+err+"</pre></font>]";
	    }
	}
	else{  //CGI
	    Map<String,byte[]> paramsMap = sessionId2InputParamsMap.get(session.getId());
	    try{
		for(Map.Entry<String,byte[]> param: paramsMap.entrySet()){
		    //todo: handle non-String values (e.g. binary file uploads)
		    addField(param.getKey(), new String(param.getValue()), testCaseData, request, namesInUse,
			     inputsSpec, inURISpec, secondarySpec, secURISpec, fixedSpec);
		}
	    } catch(IllegalArgumentException iae){
		return iae.getMessage();
	    } catch(Exception e){
		String err = e.toString()+"\n";
		for(StackTraceElement ste: e.getStackTrace()){
		    err += ste.toString()+"\n";
		}
		return "[<font color=\"red\">Error parsing input data: <pre>"+err+"</pre></font>]";
	    }
	    
	}
	//replaceFirst is to get rid of trailing commas from naive concats above
	registrationRequest.setParameter(Registration.INPUTS_PARAM, inputsSpec.toString().replaceFirst(",$",""));
	registrationRequest.setParameter(Registration.SECONDARIES_PARAM, secondarySpec.toString().replaceFirst(",$",""));
	registrationRequest.setParameter(Registration.FIXED_PARAMS, fixedSpec.toString().replaceFirst(",$",""));

	// Need logic from here to assign output type if builder rule was specified
	writeExistingOutRuleSection(request, new StringBuilder());
	String outputType = cleanTerm(request.getParameter(MOBY_OUTPUT_TYPE_PARAM));
	if(outputType == null || outputType.length() == 0){
	    cleanTerm(request.getParameter(MOBY_OUTPUT_NS_PARAM));
	}
	if(outputType == null || outputType.length() == 0){
	    outputType = getInternalParam(request, MOBY_OUTPUT_TYPE_PARAM);
	}
	if((outputType == null || outputType.length() == 0) && 
	   getInternalParam(request, MOBY_OUTPUT_NS_PARAM) != null){
	    outputType = "Object:"+getInternalParam(request, MOBY_OUTPUT_NS_PARAM);
	}

	String outputName = request.getParameter(RETURN_XPATH_PARAM);
	//outputName = outputName.replaceAll("/[^/]+/([^/]+)/.*", "$1");

	registrationRequest.setParameter(Registration.INPUT_RULE_URIS_PARAM, inURISpec.toString());
	registrationRequest.setParameter(Registration.SECONDARY_RULE_URIS_PARAM, secURISpec.toString());

	String outURI = request.getParameter(OUTPUT_RULE_URI_PARAM);
	if(outURI == null){
	    try{
		outURI = createTransformRuleAndMakeAvailable(request, outputName);
		outputType = getOutputDataTypeFromRequest(request);
	    } catch(Exception e){
		String err = e.toString()+"\n";
		for(StackTraceElement ste: e.getStackTrace()){
		    err += ste.toString()+"\n";
		}
		return "[<font color=\"red\">Error creating/registering output parsing rule: <pre>"+err+"</pre></font>]";
	    }
	}
	String outSpec = outputName+":"+outputType; // just one output for now
	registrationRequest.setParameter(Registration.OUTPUTS_PARAM, outSpec);
	String outURISpec = outputName+" "+outURI; // only one output for now
	registrationRequest.setParameter(Registration.OUTPUT_RULE_URIS_PARAM, outURISpec);

	// not sure these are necessary at the moment...
	registrationRequest.setParameter(Registration.INPUT_XSD_TYPES_PARAM, "");
	registrationRequest.setParameter(Registration.OUTPUT_XSD_TYPES_PARAM, outputName+" nsURI elementName");

	// fields = sample input *Moby* XML use for wrapping todo	
	MobyContentInstance fullTestCase = new MobyContentInstance();
	fullTestCase.put("test", testCaseData);
	StringWriter testCaseXML = new StringWriter();
	try{
	    MobyDataUtils.toXMLDocument(testCaseXML, fullTestCase);
	} catch(Exception e){
	    e.printStackTrace();
	    return "Couldn't get the XML for the sample input data:"+e.getMessage(); 
	}
	registrationRequest.setParameter(Registration.TEST_INPUT_PARAM, testCaseXML.toString());

	// The part of the doc that should always exist
	// todo: make real test of output
	String outputTypeClean = outputType;
	if(outputTypeClean.indexOf(":") != -1){
	    outputTypeClean = outputTypeClean.substring(0, outputTypeClean.indexOf(":"));
	}
	String testingXPath = "/moby:MOBY/moby:mobyContent/moby:mobyData/moby:Simple/moby:"+outputTypeClean;  
	registrationRequest.setParameter(Registration.TEST_XPATH_PARAM, testingXPath);

	// This is used for Xpath evaluation in the proxy to pull out the result section desired.
	// Ignore non-parsed results, they must have MOB rules applied to the whole (e.g. a PNG file)
	if(sessionId2dom.containsKey(session.getId())){ 
	    try{
		NamespaceContext nsContext = getNsContext(request); // find out service response namespaces
		String targetNsURI = nsContext.getNamespaceURI(XPATH_DEFAULT_NS_PREFIX);
		if(targetNsURI == null){
		    throw new Exception("Prefix "+XPATH_DEFAULT_NS_PREFIX+
					" did not map to a NS URI in the given context");
		}
		registrationRequest.setParameter(Registration.TARGET_NSURI_PARAM, targetNsURI);
	    } catch(Exception e){
		e.printStackTrace();
		return "Couldn't determine the namespace context for the service XPaths: "+e.getMessage();
	    }
	}

	String destinationURL = PROXY_URL_BASE;
	try{
	    HttpClient client = new HttpClient();
	    // debug
	    for(NameValuePair param: registrationRequest.getParameters()){
		System.err.println(param.getName()+": " +param.getValue());
	    }

	    int returnCode = client.executeMethod(registrationRequest);
	    
	    if(returnCode >= 300 && returnCode <= 400){
		HeaderElement[] hes = registrationRequest.getResponseHeader("Location").getElements();
		if(hes != null && hes.length > 0){
		    String dl = hes[0].getValue();	
		    if(dl != null && dl.trim().length() != 0){
			destinationURL = dl;
		    }
		}
	    }
	    else if(returnCode >= 400){
		return "[<font color=\"red\">Error registering service @ " + 
		    registrationRequest.getURI() + " (return code is " + 
		    returnCode + "): <pre>"+registrationRequest.getStatusLine()+
		    "</pre>"+registrationRequest.getResponseBodyAsString()+"</font>]";
	    }
	} catch(Exception e){
	    logger.log(Level.SEVERE, "Cannot register new service", e);
	    String err = e.toString()+"\n";
	    for(StackTraceElement ste: e.getStackTrace()){
		err += ste.toString()+"\n";
	    }
	    return "[<font color=\"red\">Error registering service: <pre>"+err+"</pre></font>]";
	}
	
	try{
	    URL specurl = new URL(specDocURL);
	    // A callback to tell seahawk that the new service is available  
	    // Resubmit the request to the new service so it's workflowable?
	    notifyWrappingCompletion(request,
				     specurl.getHost(),  //this must be coordinated with the proxy in future 
				     request.getParameter(Registration.MOBY_SERVICENAME_PARAM),
				     testCaseData);
	} catch(Exception e){
	    return "Registration successful, but there was a problem running the newly created Moby service.  "+
		"Please click <a href=\""+destinationURL+
		"\">here</a> to manage your services (e.g. change parameters, check error messages," +
		"delist the now public service).";
	}

	return "Registration successful!  Continue with your Seahawk analysis, " +
	    "or click <a href=\""+destinationURL+
	    "\">here</a> to manage your services (e.g. change parameter names, " +
	    "delist the now public service), or continue your Seahawk browsing with the service results.";
	
    }

    // Create specs to pass to Daggoo based on the input param contents
    private void addField(String fieldName, String exampleValue, MobyDataJob testCaseData, 
			  HttpServletRequest request, Map<String,String> namesInUse,
			  StringBuilder inputsSpec, StringBuilder inURISpec,
			  StringBuilder secondarySpec, StringBuilder secURISpec,
			  StringBuilder fixedSpec) throws Exception{
	String value = request.getParameter(fieldName);

	HttpSession session = request.getSession(false);
	Map<String,String> fields2TransformURIs = sessionId2FieldTransformMap.get(session.getId());
	Map<String,MobyDataInstance> fieldName2MobySrc = sessionId2MobySrcs.get(session.getId());

	// field found as-is (no multi-part suffixes required)
	if(value != null){
	    // literal
	    if(value.startsWith("\"") && value.endsWith("\"")){
		fixedSpec.append(makeXPath(fieldName)+":"+value.substring(1,value.length()-1)+",");
	    }
	    // secondary
	    else if(value.equals(CUSTOM_INPUT_PART_OPTION)){
		String[] customValues = getParam(request, 
						 fieldName+CUSTOM_INPUT_PART_VALUES_SUFFIX).trim().split("\n");
		secondarySpec.append(makeXPath(fieldName)+":String:"+customValues[0]+
				     ":["+XHTMLForm.join(",", customValues)+"],");
		testCaseData.put(RuleCreator.simplifyParamName(fieldName, namesInUse), 
				 new MobyDataSecondaryInstance(getSecondary(secondarySpec.toString(), namesInUse),
							       customValues[0]));
	    }
	    // secondary
	    else if(value.equals(FLOAT_INPUT_PART_OPTION) ||
		    value.equals(INT_INPUT_PART_OPTION) ||
		    value.equals(DATE_INPUT_PART_OPTION)){
		String minValue = getParam(request, fieldName+SCALAR_INPUT_PART_MIN_SUFFIX);
		String maxValue = getParam(request, fieldName+SCALAR_INPUT_PART_MAX_SUFFIX);
		String defaultValue = exampleValue; 
		if(defaultValue.equals("")){
		    if(!maxValue.equals("")){
			defaultValue = maxValue;
		    }
		    else if(value.equals(DATE_INPUT_PART_OPTION)){ //date with no range
			defaultValue = "1970-01-01";  // UNIX epoch for lack of a better choice
		    }
		    else{
			defaultValue = "0";
		    }
		}
		String type = value.equals(FLOAT_INPUT_PART_OPTION) ? "Float" : 
		    (value.equals(DATE_INPUT_PART_OPTION) ? "DateTime" : "Integer");
		String paramSpec = makeXPath(fieldName)+":"+type+":"+defaultValue+":["+minValue+","+maxValue+"]";
		//do some bounds checking
		org.biomoby.service.MobyServlet.stringToSecondaryDataTemplate(paramSpec);
		secondarySpec.append(paramSpec+",");
		testCaseData.put(RuleCreator.simplifyParamName(fieldName, namesInUse), 
				 new MobyDataSecondaryInstance(getSecondary(secondarySpec.toString(), namesInUse),
							       defaultValue));
	    }
	    else{
		throw new IllegalArgumentException("[<font color=\"red\">Error parsing input parameter " + fieldName + 
						   ": the value was neither a literal nor a secondary " +
						   "param as expected, but rather " + value + "</font>]");
	    }
	}
	// field not found as-is, check for multi-part composition of field 
	// with name-0, name-1, etc. suffixes
	else{
	    Vector<String> subFieldValues = new Vector<String>(); 
	    for(int j = 0; !getParam(request, fieldName+"-"+j).equals(""); j++){
		subFieldValues.add(getParam(request, fieldName+"-"+j));
	    }
	    // straight correspondence
	    if(subFieldValues.size() == 1){
		String val = subFieldValues.elementAt(0);
		if(val.matches(MobyTags.MOBYOBJECT+":.*")){
		    inputsSpec.append(makeXPath(fieldName)+":"+val+",");
		}
		// secondary param: todo
		else if(false){
		}
		// complex moby object
		else{
		    inputsSpec.append(makeXPath(fieldName)+":"+val+",");
		}
		// add URI from pastedData
		inURISpec.append(makeXPath(fieldName)+" "+fields2TransformURIs.get(fieldName+"-"+0)+" "); 
		testCaseData.put(RuleCreator.simplifyParamName(fieldName, namesInUse), 
				 fieldName2MobySrc.get(fieldName+"-"+0));
	    }
	    // todo: need to build an XSLT that puts together all of these 
	    // pieces to make the required input, then list all the inputs in the *Spec vars
	    else{
		// create URIs for fancy composite rules use same xpath notation, 
		// but add [0] [1], etc. to denote multipart production
		for(int j = 0; j < subFieldValues.size(); j++){
		    String val = subFieldValues.elementAt(j);
		    if(val.matches(MobyTags.MOBYOBJECT+":.*")){
			inputsSpec.append(makeXPath(fieldName+"["+j+"]")+":"+val+",");
		    }
		    // secondary param: todo
		    else if(false){
		    }
		    // complex moby object
		    else{
			inputsSpec.append(makeXPath(fieldName+"["+j+"]")+":"+val+",");
		    }
		    //add URI from pastedData
		    String fieldKey = fieldName+"-"+j;
		    String xpathKey = fieldName+"["+j+"]";
		    inURISpec.append(makeXPath(xpathKey)+" "+fields2TransformURIs.get(fieldKey)+" ");
		    testCaseData.put(RuleCreator.simplifyParamName(xpathKey, namesInUse), 
				     fieldName2MobySrc.get(fieldKey));
		}
	    }
	}
    }
    
    private MobySecondaryData getSecondary(String spec, Map<String,String> namesInUse) throws Exception{
	MobySecondaryData sec = MobyServlet.stringToSecondaryDataTemplate(spec);
	// simplify the xpath name to a Moby one
	sec.setName(RuleCreator.simplifyParamName(sec.getName(), namesInUse));
	return sec;
    }

    // for now there is only one output, so the outputName is superfluous, but keep it for future reference
    private String createTransformRuleAndMakeAvailable(HttpServletRequest request,
						       String outputName) throws Exception{
	String xpath = getParam(request,RETURN_XPATH_PARAM);
      	String dataType = getParam(request,MOBY_OUTPUT_TYPE_PARAM);
	if(dataType.indexOf(" ") != -1){ //chop off the description (anything past the initial name)
	    dataType = dataType.substring(0, dataType.indexOf(" "));
	}
	Map<String,String> memberMap = new HashMap<String,String>();
	Map<String,String> nsMap = new HashMap<String,String>();

	if(dataType.length() != 0){
	    MobyDataType dt = MobyDataType.getDataType(dataType, SeahawkOptions.getRegistry());
	    String dataTypeName = dt.getName();
	    if(dataTypeName.equals(MobyTags.MOBYSTRING) ||
	       dataTypeName.equals(MobyTags.MOBYFLOAT) ||
	       dataTypeName.equals(MobyTags.MOBYINTEGER) ||
	       dataTypeName.equals(MobyTags.MOBYBOOLEAN) ||
	       dataTypeName.equals(MobyTags.MOBYDATETIME)){  // is it a primitive? no suffix in that case...
		String memberXPath = request.getParameter(VALUE_XPATH_PARAM);
		if(memberXPath == null){
		    throw new Exception("Cannot find XPath param ("+VALUE_XPATH_PARAM+
					") for the '"+dataTypeName + "' primitive value ");
		}
		memberMap.put(MobyComplexBuilder.PRIMITIVE_VALUE_SENTINEL, fixXPath(memberXPath, request)); // "" is special member name key for primitives
	    }
	    else{
		// xpaths should be called foo-memberName foo-otherMemberName etc.
		for(MobyRelationship relationship: dt.getAllChildren()){
		    String memberName = relationship.getName();
		    String memberXPath = request.getParameter(VALUE_XPATH_PARAM+memberName);
		    if(memberXPath == null){
			throw new Exception("Cannot find XPath param ("+VALUE_XPATH_PARAM+memberName+
					    ") for '"+dataType + "' data type's member " + memberName);
		    }
		    memberMap.put(memberName, fixXPath(memberXPath, request));
		}
	    }
	}
	else{
	    // Assumes that we have ns or datatype but not both
	    dataType = MobyTags.MOBYOBJECT;
	    String ns = getParam(request,MOBY_OUTPUT_NS_PARAM);
	    if(ns.length() != 0){
		ns = ns.substring(0, ns.indexOf(" "));  //chop off desc
		nsMap.put(ns, getParam(request,VALUE_XPATH_PARAM));
	    }
	}

	// Todo: when we figure out null namespaces, pass the NamespaceContext to the rule creator, because
	// it'll need to resolve prefixes in the XPath (i.e. fixXPath will return prefix-ns mapping dependent parts).
	return RuleCreator.createLiftingRuleAndMakeAvailable(lastAxis(fixXPath(xpath, request)), xpath, dataType, nsMap, memberMap);
    }

    // truncate the full xpath into a test for the current node if it was applicable
    private String lastAxis(String xpath){
	String axis = xpath.replaceFirst("^.*/(.*)$", "$1");
	if(axis.matches("\\*\\[.*\\]")){
	    return "(self::node())" + axis.substring(1);
	}
	else if(axis.matches(".+\\[.*\\]")){
	    String name = axis.replaceFirst("^(.+)\\[.*$", "$1");
	    String conditions = axis.replaceFirst("^.+\\[(.*)]$", "$1");
	    return "(self::node())[name() = '" + name + "' and " + conditions + "]";
	}
	else{
	    return "(self::node())[name() = "+axis+"]";
	}
    }

    // Since SourceMap has a:b format for nesting, simply sub ":" for "/" to get XPath
    private String makeXPath(String sourceMapName){
	return sourceMapName.replaceAll(":","/");
    }

    // only the first word is used, the rest must be description for UI purposes only
    private String cleanTerm(String term){
	if(term != null && term.indexOf(' ') != -1){
	    term = term.substring(0, term.indexOf(' '));
	}
	return term;
    }

    private boolean isWSDL(HttpServletRequest request){
	HttpSession session = request.getSession(true);
	String[] wsdlSpec = sessionId2serviceSpec.get(session.getId()).split(" ");
	return wsdlSpec.length != 3; // 3 is length for CGI wrappers
    }

    private String wrappingHeader(HttpServletRequest request, Map<String,String> todo){
	StringBuilder result = new StringBuilder();

	HttpSession session = request.getSession(true);
	String[] wsdlSpec = sessionId2serviceSpec.get(session.getId()).split(" ");

	boolean isWSDL = isWSDL(request);
	String shortOp = isWSDL ? wsdlSpec[6] : wsdlSpec[0];
	int index = shortOp.indexOf('#');
	if(index != -1 && index != shortOp.length()-1){ 
	    // get rid of namespace qualifier if present
	    shortOp = shortOp.substring(index+1);
	}
	String opName = isWSDL ? wsdlSpec[1]+"/"+shortOp : shortOp;

	// head with title, plus script and stylesheet references for autocomplete functionality
	result.append("<html><head>\n");
	result.append("<script src=\"?"+RESOURCE_FILE_PARAM+"=moby_ajax.js\" type=\"text/javascript\"></script>\n");
	result.append("<script src=\"?"+RESOURCE_FILE_PARAM+"=autocomplete.js\" type=\"text/javascript\"></script>\n");
 	result.append("<script type=\"text/javascript\">\n");
 	result.append("function noenter() {return !(window.event && window.event.keyCode == 13); }\n");
	result.append("</script>\n");
	result.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"?"+RESOURCE_FILE_PARAM+"=mobyComplete.css\"/>\n");
	result.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"?"+RESOURCE_FILE_PARAM+"=stylesheet.css\"/>\n");
	result.append("<title>Operation "+opName+": Semantic Definitions</title>\n");
	result.append("</head>\n");

	// start of body with title and any warnings
	result.append("<body onload=\"");
	// put this first,as the other may not exist depending on the data submitted
	result.append("ajaxManager('"+MOBY_SERVICETYPE_FIELD_ID+"','?"+AUTOCOMPLETE_PARAM+"=servicetype:');"); 
	result.append("ajaxManager('"+MOBY_OUTPUT_NS_FIELD_ID+"','?"+AUTOCOMPLETE_PARAM+"=namespace:');");
	result.append("ajaxManager('"+MOBY_OUTPUT_TYPE_FIELD_ID+"','?"+AUTOCOMPLETE_PARAM+"=datatype:');");
	result.append("\">\n");
	result.append("<h2>Operation <i>"+opName+"</i> Semantic Definitions</h2>");

	// hidden wsdl name?
	if(todo.size() != ALL_MISSING_COUNT){
	    result.append("<div class=\"warning\">Please correctly fill in the " +todo.size()+ " field" +
			  (todo.size() == 1 ? "" : "s") + " noted in red below.</div>");
	}

	// start of form, include the only param not overrideable, the xptr context for ns resolution
	result.append("<form action=\"?"+CONTEXT_XPTR_PARAM+"="+getParam(request, CONTEXT_XPTR_PARAM)+"\" method=\"POST\">");

	return result.toString();
    }

    private String wrappingOutputSelection(HttpServletRequest request, Map<String,String> todo){
	StringBuilder result = new StringBuilder();

	String xPath = getParam(request,RETURN_XPATH_PARAM);

	result.append("<h3>Output Selection</h3>\n");
	result.append("Selection XPath <input name=\""+RETURN_XPATH_PARAM+"\" value=\""+escape(xPath)+
		      "\" size=\""+(xPath.length()+5)+"\"/> ");

	writeDataTypeExample(request, result);

	// Apply the xpath to the dom to get the scope of the results
	String ruleURI = getParam(request,OUTPUT_RULE_URI_PARAM);
	if(ruleURI.length() != 0){
	    writeExistingOutRuleSection(request, result);
	}
	else{
	    writeNewOutRuleSection(request, result);
	}
	return result.toString();
    }

    /**
     * Before we print the table with the xpath evaluation results, see if a datatype has been selected,
     * and if so, show an example of the moby data and full ontology lineage.
     */
    private void writeDataTypeExample(HttpServletRequest request, StringBuilder result){
	HttpSession session = request.getSession(true);
	MobyContentGUI gui = sessionId2gui.get(session.getId());

	String dataType = getOutputDataTypeFromRequest(request);
	try{
	    result.append(getDataTypeSkeletonStructure(dataType, gui));
	} catch(Exception e){
	    e.printStackTrace();
	    logger.log(Level.WARNING, "While try to get datatype example XML", e);
	}

	if(dataType.length() != 0){
	    if(dataType.startsWith(MobyTags.MOBYOBJECT+":")){
		dataType = dataType.substring(7);
		MobyNamespace ns = MobyNamespace.getNamespace(dataType, SeahawkOptions.getRegistry());
		result.append("<br/><i>Full Namespace Description: " + (ns != null ? ns.getDescription() : "N/A")
			      +"</i><br>\n");
	    }
	    else{
		MobyDataType dt = MobyDataType.getDataType(dataType, SeahawkOptions.getRegistry());
		result.append("<br/><i>Full Datatype Description: " + (dt != null ? dt.getDescription() : "N/A")
			      +"</i><br>\n");
		result.append("<i>Full Lineage: " + (dt != null ? getLineage(dt) : "N/A") +"</i>\n");
	    }
	}
    }

    private String getOutputDataTypeFromRequest(HttpServletRequest request){
	String dataType = getParam(request,MOBY_OUTPUT_TYPE_PARAM);
	if(dataType.indexOf(" ") != -1){ //chop off the description (anything past the initial name)
	    dataType = dataType.substring(0, dataType.indexOf(" "));
	}
	if(dataType.length() == 0 && getParam(request,MOBY_OUTPUT_NS_PARAM).length() != 0){
	    String ns = getParam(request,MOBY_OUTPUT_NS_PARAM);
	    if(ns.indexOf(" ") != -1){
		dataType = ns.substring(0, ns.indexOf(" "));
	    }
	    else{
		dataType = ns;
	    }
	    dataType = MobyTags.MOBYOBJECT+":"+dataType;
	}
	return dataType;
    }

    /** If the user wants to use an existing rule, we show them the results here*/
    private void writeExistingOutRuleSection(HttpServletRequest request, StringBuilder result){
	String ruleURI = getParam(request, OUTPUT_RULE_URI_PARAM);

	// The URI should be either an LSID or a URL
	URL ruleURL = null;
	if(LSIDResolver.isLSID(ruleURI)){
	    try{
		ruleURL = getResolver().resolveDataURL(ruleURI);
	    } catch(Exception e){
		result.append("[Internal error: the LSID given to transform the output (" +
			      ruleURI+") could not be resolved: " + e + "]");
		return;
	    }
	}
	else{
	    try{
		ruleURL = new URL(ruleURI);
	    } catch(Exception e){
		result.append("[Internal error: the URI given to transform the output (" +
			      ruleURI+") is not an LSID, or a well-formed URL: " + e + "]");
		return;	
	    }
	}

	MobyPrimaryDataSimple mobyObj = null;
	try{
	    mobyObj = MobyClient.getObjectProduced(ruleURL, ruleURI, SeahawkOptions.getRegistry());
	} catch(Exception e){
	    result.append(e.getMessage());
	    return;
	}

	result.append("<br/>Creating output using the existing Mobifying rule " + ruleURI + "<br/>"+
		      "<input type=\"hidden\" name=\""+OUTPUT_RULE_URI_PARAM+"\" value=\""+ruleURI+"\"/>\n"+
		      "<input type=\"hidden\" name=\""+VALUE_XPATH_PARAM+"\" value=\""+
		      getParam(request, VALUE_XPATH_PARAM)+"\">");

	MobyDataType dt = mobyObj.getDataType();
	MobyNamespace[] nss = mobyObj.getNamespaces();
	// Override any existing setting for the data type with the info from the generating rule
	if(dt.getName().equals(MobyTags.MOBYOBJECT)){ //just a base object with a namespace
	    if(nss.length == 1){  // if only one ns, we can unambiguously define it...
		setInternalParam(request, MOBY_OUTPUT_TYPE_PARAM, "");
		setInternalParam(request, MOBY_OUTPUT_NS_PARAM, nss[0].getName());
	    }
	}
	else{  // actual data type, not just a namespace
	    setInternalParam(request, MOBY_OUTPUT_TYPE_PARAM, dt.getName());
	    if(nss.length == 1){
		setInternalParam(request, MOBY_OUTPUT_NS_PARAM, nss[0].getName());		
	    }
	    else{
		setInternalParam(request, MOBY_OUTPUT_NS_PARAM, "");
	    }
	}
    }

    private LSIDResolver getResolver(){
	if(lsidResolver == null){
	    lsidResolver = new LSIDResolver();
	}
	return lsidResolver;
    }

    /* Part of form used to define a new xml schema -> moby rule using tables and xpaths */
    private void writeNewOutRuleSection(HttpServletRequest request, StringBuilder result){
	HttpSession session = request.getSession(true);
	Node domNode = sessionId2dom.get(session.getId());
	MobyContentGUI gui = sessionId2gui.get(session.getId());

	String xPath = getParam(request,RETURN_XPATH_PARAM);

	// Sometime literal docs have xsi:type attributes, define the namespace just in case
	if(isLiteral(request)){
	    Element e = (Element) (domNode instanceof Element ? domNode : domNode.getFirstChild());
	    if(e.getAttribute("xmlns:xsi").length() == 0){
		e.setAttributeNS("http://www.w3.org/2000/xmlns/", 
				 "xmlns:xsi", 
				 "http://www.w3.org/2001/XMLSchema-instance");

		DOMResult domResult = new DOMResult();
		try{
		    synchronized(nullTransformer){  //just serializes
			nullTransformer.transform(new DOMSource(domNode), domResult);
		    }
		    domNode = domResult.getNode();
		    sessionId2dom.put(session.getId(), domNode);
		    e = (Element) (domNode instanceof Element ? domNode : domNode.getFirstChild());
		} catch(Exception ex){
		    System.err.println("Failed to reserialize: "+ex.getMessage());
		    ex.printStackTrace();
		}
	    }
	    // NodeList nl = e.getElementsByTagName("*");
// 	    for(int i = 0; i < nl.getLength(); i++){
// 		Element child = (Element) nl.item(i);
// 		//System.err.println("Element " + child.getNodeName() + " ns " + child.getNamespaceURI() + 
// 		//		   " for prefix " + child.getPrefix());
// 	    }
	}

	// The namespace prefixes used in the selection xpath will use the following node's prefix:ns mappings
	NamespaceContext nsContext = null;
	try{
	    // do this internally, because an XPath will not give the node in the DOM context,
	    // just a copy of the target node.  This is useless for namespace resolution we are
	    // trying to do via parent traversal for xmlns attributes...
	    nsContext = getNsContext(request);
	    //nsContext.setPrefix(, "");
	}catch(Exception e){
	    result.append("[<font color=\"red\" style=\"bold\">Internal error: the NS context XPointer is " +
			  "syntactically incorrect: " + e + ".. Please notify gordonp@ucalgary.ca</font>"+ 
			  "]\n"); // <input type=\"submit\" value=\"Reselect\"/><br/>\n");
	    e.printStackTrace();
	}

	NodeList xPathResults = null;
	int numResults = 0;
	try{
	    XPath xPathObj = xPathFactory.newXPath();
	    xPathObj.setNamespaceContext(nsContext);
	    System.err.println("About to evaluate " + fixXPath(xPath, request));
	    xPathResults = (NodeList) xPathObj.evaluate(fixXPath(xPath, request), domNode, XPathConstants.NODESET);
	    numResults = xPathResults.getLength();
	    if(numResults == 0){
		result.append("[<font color=\"red\" style=\"bold\">the selection XPath ("+fixXPath(xPath, request)+") does " +
			      "not select any data, please modify</font>"+ 
			      "]\n"); // <input type=\"submit\" value=\"Reselect\"/><br/>\n");
	    }
	    else{
		result.append("["+numResults+" result"+(numResults==1?"":"s")+
			      "]<br/>\n"); // <input type=\"submit\" value=\"Reselect\"/><br/>\n");
	    }
	}catch(Exception e){
	    result.append("[<font color=\"red\" style=\"bold\">the selection XPath was syntactically " +
			  "invalid, please modify"+e+"</font>"+
			  "]\n"); // <input type=\"submit\" value=\"Reselect\"/><br/>\n");
	    e.printStackTrace();
	}

	//todo output xsd type:  xPathResults.item(0).getNamespaceURI(), xPathResults.item(0).getLocalName;

	Vector<String> valXPathSuggestions = (numResults == 0 ? new Vector<String>() : RuleCreator.suggestValueXPathsFromSelectionXPath(xPath, xPathResults.item(0)));
	String valueXPath = getParam(request, VALUE_XPATH_PARAM);
	String selectOptions = null;
	boolean suggestable = false;
	// is it either blank, or one of the existing suggestions?
	if(valueXPath.length() == 0 || getSuggestionXPath(valXPathSuggestions,valueXPath) != null){
	    selectOptions = "<select name=\""+VALUE_XPATH_PARAM+
		"\" title=\"Expand the drop-down list for XPath options and English tooltips\"\" " +
		"onchange=\"this.form.submit();\">\n"; 
	    for(String suggestedValue: valXPathSuggestions){
		String[] s = suggestedValue.split(" -- "); // stored format is xpath -- English desc
		if(s[0].equals(valueXPath)){  //the user has explicitly picked a dropdown value, so select it
		    selectOptions += " <option title=\""+s[1]+"\" value=\""+s[0]+"\" selected=\"selected\">"+s[0]+"</option>\n";    
		}
		else{
		    selectOptions += " <option title=\""+s[1]+"\" value=\""+s[0]+"\">"+s[0]+"</option>\n";
		}
	    }
	    selectOptions += " <option>"+CUSTOM_VALUE_XPATH_OPTION+"</option>\n";
	    selectOptions += "</select>\n";

	    // If no value already, let's suggest the first one
	    if(valueXPath.length() == 0){
		if(valXPathSuggestions.size() == 0){
		    valueXPath = "";
		}
		else{
		    valueXPath = valXPathSuggestions.elementAt(0).replaceAll("(.*) -- .*", "$1");		    
		    valXPathSuggestions.removeElementAt(0);
		    suggestable = true;
		}
	    }
	}
	// a custom XPath
        else{
	    //just switched from dropdown menu to free-text, start with a blank
	    if(valueXPath.equals(CUSTOM_VALUE_XPATH_OPTION)){
		valueXPath = "";  
	    }
	    selectOptions = "<input name=\""+VALUE_XPATH_PARAM+"\" value=\""+escape(valueXPath)+"\""+
		"title=\"Leave blank and recalculate to get the XPath suggestions dropdown\"/>\n";
	}
	//result.append("       <input type=\"submit\" value=\"Recalculate values\"/><br/>\n"); 
	result.append("<table border=\"1\">\n <tr><th>Selection #</th><th>Values "+
		      (selectOptions == null ? "" : selectOptions) +
		      "</th></tr>\n");
	// Apply the extraction rule to see the resulting values
	int numValuesFound = 0;
	StringBuilder rows = new StringBuilder();
	for(int i = 0; i < numResults; i++){
	    XPath xPathObj = xPathFactory.newXPath();
	    //xPathObj.setNamespaceContext(nsContext);
	    NodeList valueXPathResults = null;
	    try{
		valueXPathResults = (NodeList) xPathObj.evaluate(fixXPath(valueXPath, request), 
								 xPathResults.item(i), 
								 XPathConstants.NODESET);
	    }catch(Exception e){
		// The XPath may return a STRING, in which case the above would fail. Try STRING instead
		try{
		    String valueXPathString = (String) xPathObj.evaluate(fixXPath(valueXPath, request), 
									 xPathResults.item(i), 
									 XPathConstants.STRING);
		    valueXPathResults = new WrappingServlet.MyNodeList();
		    ((WrappingServlet.MyNodeList) valueXPathResults).add(
				      docBuilder.newDocument().createTextNode(valueXPathString));
		} catch(Exception e2){
		    rows.append(" <tr><td>"+getSelectionXPathLink(xPath, i+1)+"</td><td><font color=\"red\" style=\"bold\">["+
				(valueXPath == null ? "Enter your custom XPath at the top of the column" : 
				 "The value XPath ("+valueXPath+") was syntactically invalid, please modify.  The result must " +
				 "be a nodeset or a string</font>")+
				"]</td></tr>\n");
		    e.printStackTrace();
		    e2.printStackTrace();
		    continue;
		}
	    }
	    if(valueXPathResults.getLength() == 0){
		rows.append(" <tr><td>"+getSelectionXPathLink(xPath, i+1)+"</td><td>[<font color=\"blue\" style=\"bold\">" +
			    "No value found via the given XPath, will be skipped]</font></td></tr>\n");
	    }
	    else{
		numValuesFound++;
		rows.append(" <tr><td rowspan=\""+valueXPathResults.getLength()+"\">"+getSelectionXPathLink(xPath, i+1)+"</td><td>"+
			    getScalar(valueXPathResults.item(0)));
		for(int j = 1; j < valueXPathResults.getLength(); j++){
		    rows.append("</td></tr>\n <tr><td>"+getScalar(valueXPathResults.item(j)));
		}
		rows.append("</td></tr>\n");
	    }

	    // If we got no values at all in the loop, and we're using the dropdown, 
	    // try a different suggested value xpath, which may pick something up.
	    if(suggestable && i == numResults-1){
		if(numValuesFound != 0){
		    // the suggestion worked.  make sure we select the suggestion in the drop down menu
		    int pos = result.indexOf(">"+valueXPath+"</option>");
		    if(pos == -1){
			System.err.println("Whoa! Got a suggestion not on the suggestion list: "+valueXPath);
		    }
		    else{
			result.replace(pos, pos+1, " selected=\"selected\">"); 
		    }
		}
		// next suggestion please
		else if(valXPathSuggestions.size() != 0){
		    valueXPath = valXPathSuggestions.elementAt(0).replaceAll("(.*) -- .*", "$1");
		    valXPathSuggestions.removeElementAt(0);
		    rows = new StringBuilder(); // clear out the 'skipping' messages
		    i = -1;  //restart the loop
		}
		// else none of the suggestions works :-(
	    }
	}

	result.append(rows.toString()+"</table>\n");
    }

    private String getLineage(MobyDataType dataType){
	StringBuilder sb = new StringBuilder();
	for(MobyDataType dt: dataType.getLineage()){
	    sb.append("> <acronym title=\""+dt.getDescription()+"\">"+dt.getName()+"</acronym> ");
	}
	return sb.toString();
    }

    private String getLineage(String serviceTypeName){
	MobyServiceType serviceType = MobyServiceType.getServiceType(serviceTypeName, SeahawkOptions.getRegistry());
	if(serviceType == null){
	    return "";
	}
	StringBuilder sb = new StringBuilder("Lineage ");
	for(MobyServiceType dt: serviceType.getLineage()){
	    sb.append(">"+dt.getName());
	}
	return sb.toString();
    }

    private String getDataTypeSkeletonStructure(String dataTypeName, MobyContentGUI gui) throws Exception{
	if(dataTypeName == null || dataTypeName.trim().length() == 0){
	    return "";
	}

	StringBuilder result = new StringBuilder();
	
	result.append("<br/>For your reference, here's an example of your chosen Moby output type "+
		      dataTypeName+":<font size=\"-1\"><pre>");
	try{
	    // Try to find an example of the object
	    // todo: make this not gimpy hard-coded
	    URL url = new URL("http://www.visualgenomics.ca/gordonp/moby/specimens/"+dataTypeName+".xml");
	    MobyContentInstance sample = MobyDataUtils.fromXMLDocument(url.openStream());

	    find: 
            for(MobyDataJob job: sample.values()){
		for(MobyDataInstance data: job.values()){
		    if(data instanceof MobyPrimaryData &&
		       ((MobyPrimaryData) data).getDataType().getName().equals(dataTypeName)){
			data.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
			// The replaceFirsts below are to eliminate the top-level element's irrelevant attributes
			result.append(data.toXML().replaceAll("<","&lt;").replaceAll(" xml:space=\"preserve\"","")
				                                         .replaceFirst(" articleName=\".*?\"","")
				                                         .replaceFirst(" namespace=\".*?\"","")
				                                         .replaceFirst(" id=\".*?\"",""));
			break find; // labeled break for outer loop
		    }
		}
	    }
	} catch(Exception e){
	    // print a skeleton xml instead, since an example hasn't been found
	    if(dataTypeName.startsWith(MobyTags.MOBYOBJECT+":")){
		//MobyNamespace type = gui.getMobyCentralImpl().getNamespace(dataTypeName.substring(MobyTags.MOBYOBJECT.length()+1));
		//todo: look through MOB rules for a regex?
	    }
	    else{
		MobyDataType type = MobyDataType.getDataType(dataTypeName, SeahawkOptions.getRegistry());
		result.append("&lt;"+type.getName()+">\n");
		for(MobyRelationship relationship: type.getAllChildren()){
		    result.append(printRelationship(relationship, "  ", gui.getMobyCentralImpl()));
		}
		result.append("&lt;/"+type.getName()+">\n");
	    }
	}
	result.append("</pre></font>\n");
	return result.toString().replaceAll("articleName=\"(.+?)\"", "articleName=\"<b>$1</b>\"");
    }

    private String printRelationship(MobyRelationship relationship, String indent, Central mobyCentral) throws Exception{
	StringBuilder result = new StringBuilder();
	String dataTypeName = relationship.getDataTypeName();
	if(dataTypeName.equals(MobyTags.MOBYOBJECT)){
	    result.append(indent+"&lt;"+MobyTags.MOBYOBJECT+" articleName=\""+relationship.getName()+"\" namespace=\"??\" id=\"??\"/>\n");
	}
	else if(dataTypeName.equals(MobyTags.MOBYSTRING) ||
		dataTypeName.equals(MobyTags.MOBYFLOAT) ||
		dataTypeName.equals(MobyTags.MOBYINTEGER) ||
		dataTypeName.equals(MobyTags.MOBYBOOLEAN) ||
		dataTypeName.equals(MobyTags.MOBYDATETIME)){
	    result.append(indent+"&lt;"+dataTypeName+" articleName=\""+relationship.getName()+"\">\n");
	}
	else{
	    result.append(" &lt;"+relationship.getDataTypeName()+" articleName=\""+relationship.getName()+"\"&gt;");
	    if(relationship.getRelationshipType() == Central.iHAS){
		result.append("&nbsp;&lt;!-- Any number of these --&gt;");
	    }
	    result.append("\n");
	    //recurse to print descendants
	    MobyDataType type = mobyCentral.getDataType(dataTypeName);
	    for(MobyRelationship child: type.getAllChildren()){
		result.append(printRelationship(child, indent+"  ", mobyCentral));
	    }
	    result.append(indent+"&lt;"+relationship.getDataTypeName()+"&gt;\n");	
	}
	return result.toString();
    }
	
    // returns the input XPath, unless it's not in the suggestions, in which case null is returned
    private String getSuggestionXPath(Vector<String> valXPathSuggestions, String valueXPath){
	for(String suggestedValue: valXPathSuggestions){
	    String[] s = suggestedValue.split(" -- "); // stored format is xpath -- English desc
	    if(s[0].equals(valueXPath)){
		return valueXPath;
	    }
	}
	return null;
    }



    private NamespaceContext getNsContext(HttpServletRequest request) throws Exception{
	HttpSession session = request.getSession(true);
	Node domNode = sessionId2dom.get(session.getId());

	String contextNodeXPtr = getParam(request,CONTEXT_XPTR_PARAM);	
	if(contextNodeXPtr.length() == 0){
	    if(sessionId2lastXPtr.containsKey(session.getId())){
		contextNodeXPtr = sessionId2lastXPtr.get(session.getId());
	    }
	}
	else{
	    sessionId2lastXPtr.put(session.getId(), contextNodeXPtr);
	}
	// do this internally, because an XPath will not give the node in the DOM context,
	// just a copy of the target node.  This is useless for namespace resolution we are
	// tryin to do via parent traversal for xmlns attributes...
	Node contextNode = findXPtr(domNode, contextNodeXPtr);
	return new NamespaceContextImpl(contextNode, XPATH_DEFAULT_NS_PREFIX);
    }

    private boolean isLiteral(HttpServletRequest request){
	HttpSession session = request.getSession(true);
	return sessionId2serviceSpec.get(session.getId()).split(" ")[9].equals("literal");
    }

    private String fixXPath(String xPath, HttpServletRequest request){
	if(isLiteral(request)){
	    // Special case: qualify unqualified elements in the XPath, otherwise they will fail
	    // we do this here rather than on the referring page because the regex code
	    // used to generate the referring page links can't make the prefixes easily
	    //return xPath.replaceAll("/([a-zA-Z_0-9\\-]+)(?=/|$|\\[)", "/"+XPATH_DEFAULT_NS_PREFIX+":$1");
	    return xPath.replaceAll("/([a-zA-Z_0-9\\-]+)(?=/|$)", "/*[local-name() = '$1']")
		.replaceAll("/([a-zA-Z_0-9\\-]+)\\[", "/*[local-name() = '$1' and ");
	}
	else{
	    return xPath;  //rpc nodes usually have no namespace
	}
    }

    // Resolve /1/1/2/1/3 style xpointers to the DOM node they indicate
    private Node findXPtr(Node domNode, String xPtr) throws Exception{
	String[] path = xPtr.split("/");
	Node currentNode = domNode;
	if(path[0].length() != 0){
	    throw new Exception("The XPointer did not start with a '/', only '/1/1/2/1' style XPtrs are acceptable");
	}
	for(int i = 1; i < path.length; i++){
	    int desiredCount = Integer.parseInt(path[i]); // will throw exception if path element is not an integer
	    int elementCount = 0;
	    boolean found = false;
	    NodeList children = currentNode.getChildNodes();
	    for(int j = 0; j < children.getLength(); j++){
		if(children.item(j) instanceof Element){
		    if(desiredCount == ++elementCount){
			currentNode = children.item(j);
			found = true;
			break;
		    }
		}
	    }
	    if(!found){
		throw new Exception("The XPointer '" + xPtr +"' could not be found, at depth "+i+
				    ", the maximum child # was " + elementCount + " but " + 
				    desiredCount + " was requested");
	    }
	}
	return currentNode;
    }

    private String getSelectionXPathLink(String xPath, int i){
	return "<a href=\"?"+RAW_XPATH_PARAM+"="+escape(xPath)+"["+i+"]\">"+i+"</a>";
    }

    // Used to convert node list results into text that can be used to fill Moby data fields
    private String getScalar(Object o){
	if(o instanceof Attr){
	    return ((Attr) o).getValue();
	}
	else if(o instanceof Text){
	    return ((Text) o).getNodeValue().replaceAll("(\\S{60})","$1\n").replaceAll(" ", "&nbsp;");
	}

	return "[<font color=\"red\" style=\"bold\">Could not convert the XPath result (class " + 
	    o.getClass() + ") into a string, please modify the value-defining XPath]</font>]";
    }

    private String wrappingOutputType(HttpServletRequest request, Map<String,String> todo){
	StringBuilder result = new StringBuilder();

	result.append("<h3>Output Type ");
	if(getInternalParam(request, MOBY_OUTPUT_TYPE_PARAM) != null ||
	   getInternalParam(request, MOBY_OUTPUT_NS_PARAM) != null){
	    result.append("</h3><b>"); // picked by the rule spec, which would have updted the POST params by this point
	    String type = getInternalParam(request, MOBY_OUTPUT_TYPE_PARAM);
	    if(type != null && type.length() != 0){
		result.append(type+"<input type=\"hidden\" name=\""+MOBY_OUTPUT_TYPE_PARAM+"\" value=\""+type+"\"/>");
	    }
	    else{
		type = getInternalParam(request, MOBY_OUTPUT_NS_PARAM);
		if(type == null || type.length() == 0){
		    result.append("[Internal error: transformation rule was specified, but no data type was automatically set]");
		    return result.toString();
		}
		// todo: how to show multiple possible namespaces?
		result.append("Object in namespace "+type+"<input type=\"hidden\" name=\""+MOBY_OUTPUT_NS_PARAM+"\" value=\""+type+"\"/>");		
	    }
	    result.append("</b> as defined by your Seahawk data selection");
	    return result.toString();
	}
        // else user must select
	result.append("(choose a button on the left below, and a value)</h3>\n");

	// ns:id ...
	result.append("<input type=\"radio\" name=\""+OUTPUT_RADIO_PARAM+"\" value=\""+
		      OUTPUT_RADIO_BASE+"\"/>A basic ID in namespace: ");
	result.append("<input name=\""+MOBY_OUTPUT_NS_PARAM+"\" size=\"80\" id=\""+MOBY_OUTPUT_NS_FIELD_ID+
		      "\" type=\"text\" autocomplete=\"off\" value=\""+getParam(request, MOBY_OUTPUT_NS_PARAM)+
		      "\" />" +
		      getErr(todo,MOBY_OUTPUT_NS_PARAM)+"\n");
	result.append("<input type=\"hidden\" id=\""+MOBY_OUTPUT_NS_FIELD_ID+"hidden\"/>\n");
	result.append("<div id=\""+MOBY_OUTPUT_NS_FIELD_ID+"Complete\" class=\"MobyCompleteBox\" " +
		      "style=\"visibility:hidden;display:block;\"></div><br/>\n");

	// ...or more complex datatype option...
	result.append("<input type=\"radio\" name=\""+OUTPUT_RADIO_PARAM+"\" value=\""+
		      OUTPUT_RADIO_OTHER+"\"/>A Moby datatype: ");
	result.append("<input name=\""+MOBY_OUTPUT_TYPE_PARAM+"\" size=\"80\" id=\""+MOBY_OUTPUT_TYPE_FIELD_ID+
		      "\" type=\"text\" autocomplete=\"off\" value=\""+getParam(request,MOBY_OUTPUT_TYPE_PARAM) +
		      "\"/>"+getErr(todo,MOBY_OUTPUT_TYPE_PARAM)+"\n");
	result.append("<input type=\"hidden\" id=\""+MOBY_OUTPUT_TYPE_FIELD_ID+"hidden\"/>\n");
	result.append("<div id=\""+MOBY_OUTPUT_TYPE_FIELD_ID+"Complete\" class=\"MobyCompleteBox\" " +
		      "style=\"visibility:hidden;display:block;\"></div>\n");

	// ...or if the service had no Moby input to it, we can use the output to create a secondary
	// parameter (e.g. enumeration) for another operation in the same WSDL
	if(isNullaryOp(request)){
	    String[] serviceNames = getOtherParameterizedServices(request);
	    if(serviceNames.length != 0){
		result.append("<input type=\"radio\" name=\""+OUTPUT_RADIO_PARAM+"\" value=\""+OUTPUT_RADIO_SECONDARY+
			      "\"/> A parameter to another WSDL operation:\n");
		result.append("<select name=\""+MOBY_OUTPUT_OP_PARAM+"\">\n");
		for(String serviceName: serviceNames){
		    result.append(" <option>"+serviceName+"</option>\n");
		}
		result.append("</select>\n");
	    }
	}

	return result.toString();
    }

    //todo: isNullaryOp, getOtherParameterizedServices
    private boolean isNullaryOp(HttpServletRequest request){
	HttpSession session = request.getSession(true);
	SourceMap fields = sessionId2SourceMap.get(session.getId());

	return fields != null && fields.isEmpty();
    }
    
    /**
     * This method is to be called just before submission of data to the Web Service,
     * so that the PBE system can record the input for data type analysis later in 
     * the wrapping process.
     */
    public void setInputSource(HttpServletRequest request, SourceMap sourceMap){
	HttpSession session = request.getSession(true);
	sessionId2SourceMap.put(session.getId(), sourceMap);
    }

    /**
     * This method is to be called just before submission of data to the CGI,
     * so that the PBE system can record the input for data type analysis later in 
     * the wrapping process.
     */
    public void setInputParams(javax.servlet.http.HttpServletRequest submissionRequest, 
			       Map<String,byte[]> httpParams,
			       List<String> hiddenParams){
	HttpSession session = submissionRequest.getSession(true);
	sessionId2InputParamsMap.put(session.getId(), httpParams);
	sessionId2HiddenParamsList.put(session.getId(), hiddenParams);
    }

    // todo: Look back to see if the WSDl has any operations that results could be piped to
    private String[] getOtherParameterizedServices(HttpServletRequest request){
	return new String[]{};
    }

    public void setParameter(javax.servlet.http.HttpSession session, String paramName, String paramValue){
	if(paramValue != null){
	    if(WrappingServlet.SERVICE_SPEC_PARAM.equals(paramName)){
		sessionId2serviceSpec.put(session.getId(), paramValue);
	    }
	    else if(WrappingServlet.SRC_PARAM.equals(paramName)){
		sessionId2specLoc.put(session.getId(), paramValue);
	    }
	}
    }

//     private String[] addParameterizedService(HttpServletRequest request, String serviceName){
	
//     }

    private String wrappingInputTypes(HttpServletRequest request, Map<String,String> todo){
	if(isWSDL(request)){
	    return wrappingInputTypesWS(request, todo);
	}
	else{
	    return wrappingInputTypesCGI(request, todo);
	}
    }

    private String wrappingInputTypesCGI(HttpServletRequest request, Map<String,String> todo){
	HttpSession session = request.getSession(true);
	StringBuilder result = new StringBuilder();
	result.append("<h3>Input Types</h3>\n");

	Map<String,byte[]> params = sessionId2InputParamsMap.get(session.getId());
	List<String> hidden = sessionId2HiddenParamsList.get(session.getId());
	if(params == null){
	    result.append("Internal error: the input source was not recorded<br/>\n");
	}
	else if(params.isEmpty()){
	    result.append("This service does not have any input parameters, " +
			  "therefore nothing needs to be specified here.\n");
	}
	else{
	    for(String paramName: params.keySet()){
		// Assume that hidden fields are not to be manipulated manually
		if(hidden.contains(paramName)){
		    result.append("<input type=\"hidden\" name=\""+paramName+"\" value=\"&quot;"+
				  (new String(params.get(paramName)))+ //should be okay as hidden params are always text
				   "&quot;\"/>\n");
		}
		else{
		    result.append(getInputs(request, paramName, params.get(paramName)));
		}
	    }
	}
	return result.toString();
    }

    private String wrappingInputTypesWS(HttpServletRequest request, Map<String,String> todo){
	HttpSession session = request.getSession(true);
    
	SourceMap fields = sessionId2SourceMap.get(session.getId());

	Document inDoc = null;
	try{
	    inDoc = docBuilder.parse(new StringBufferInputStream(fields.toString()));
	} catch(Exception e){
	    String err = e.toString()+"\n";
	    for(StackTraceElement ste: e.getStackTrace()){
		err += ste.toString()+"\n";
	    }
	    return "[<font color=\"red\">Error parsing input data XML: <pre>"+err+"</pre></font>]";
	}

	StringBuilder result = new StringBuilder();

	result.append("<h3>Input Types</h3>\n");

	if(fields == null){
	    result.append("Internal error: the input source was not recorded<br/>\n");
	}
	else if(fields.isEmpty()){
	    result.append("This service does not have any input parameters, " +
			  "therefore nothing needs to be specified here.\n");
	}
	else{
	    NodeList children = inDoc.getDocumentElement().getChildNodes();
	    for(int i = 0; i < children.getLength(); i++){
		if(children.item(i) instanceof Element){
		    result.append(getInputs(request, (Element) children.item(i), ""));
		}
	    }
	}
	return result.toString();
    }
    //for CGI input
    private String getInputs(HttpServletRequest request, String paramName, byte[] paramValue){
	StringBuilder result = new StringBuilder();
	result.append("<table><tr><td>"+paramName);  //ensure all of the spec for one input ends up on the same line

	//todo: handle binary data
	String inputText = new String(paramValue);
	if(inputText.length() != 0){
	    result.append(" ("+getContext(inputText,"",30)+"):</td>");
	    //guess the type
	    result.append(getInputChoices(request, paramName, inputText));
	}
	result.append("</tr></table>\n");
	return result.toString();
    }
    //for WS input
    private String getInputs(HttpServletRequest request, Element input, String tab){
	StringBuilder result = new StringBuilder();

	result.append("<table><tr><td>");  //ensure all of the spec for one input ends up on the same line
	result.append(tab+input.getNodeName());

	String inputText = input.getTextContent().trim();
	if(inputText.length() != 0){
	    result.append(" ("+getContext(inputText,"",30)+"):</td>");
	    //guess the type
	    result.append(getInputChoices(request, getInputFieldName(input), inputText));
	}
	
	result.append("</tr>\n");

	NodeList children = input.getChildNodes();
	for(int i = 0; i < children.getLength(); i++){
	    result.append("<tr>");
	    if(children.item(i) instanceof Element){
		result.append(getInputs(request, (Element) children.item(i), tab+"&nbsp;&nbsp;"));
	    }
	    result.append("</tr>");
	}
	result.append("</table>");
	return result.toString();
    }

    /**
     * Called by source of data going into the browser, i.e. Seahawk
     */
    public void dataCopied(MobyDataInstance source, String copiedValue, String transformRuleURI){
	lastCopiedSource = source;
	lastCopiedValue = copiedValue;
	lastCopiedRuleURI = transformRuleURI;
	doUnificationIfPossible();
    }

    public void dataPasted(HttpServletRequest request){
	HttpSession session = request.getSession(false);
	if(session == null){
	    logger.log(Level.WARNING, "dataPasted got called without a session id");
	    return;
	}
	lastPastedSessionId = session.getId();
	doUnificationIfPossible();
    }

    /**
     * To avoid a race condition between dataCopied() and dataPasted(), whose origins are 
     * Seahawk and Javascript on the form respectively, both methods call this method
     * so that their call order is not important.
     */
    protected synchronized void doUnificationIfPossible(){
	if(lastPastedSessionId == null || lastCopiedSource == null){
	    return;  //shouldn't have been called
	}

	Map<MobyDataInstance,String[]> pastedData = sessionId2PastedData.get(lastPastedSessionId);
	if(pastedData == null){ //first pasted data for the session
	    pastedData = new HashMap<MobyDataInstance,String[]>();
	    sessionId2PastedData.put(lastPastedSessionId, pastedData);
	}
	
	pastedData.put(lastCopiedSource,new String[]{lastCopiedValue,lastCopiedRuleURI});
	System.err.println("Pasted " + lastCopiedValue + " using transform " + 
			   lastCopiedRuleURI + " from Seahawk source " + lastCopiedSource.toXML());
	
	if(lastCopiedSource instanceof MobyPrimaryData){
	    MobyNamespace[] nss = ((MobyPrimaryData) lastCopiedSource).getNamespaces();
	    if(MobyTags.MOBYOBJECT.equals(((MobyPrimaryData) lastCopiedSource).getDataType().getName()) &&
	       nss != null && nss.length > 0){
		setStatus("Paste of "+nss[0].getName()+ " type ID noted");
	    }
	    else{
		setStatus("Paste of "+((MobyPrimaryData) lastCopiedSource).getDataType().getName()+" object noted");
	    }
	}
	else if(lastCopiedSource instanceof MobySecondaryData){
	    setStatus("Paste of secondary param "+((MobyPrimaryData) lastCopiedSource).getDataType()+" noted");
	}
	else{
	    logger.log(Level.WARNING, "Got unexpected type (not MobyPrimaryData or MobySecondaryData) in paste: "+
		       lastCopiedSource.getClass().getName());
	    setStatus("Paste of "+lastCopiedSource.getClass().getName()+" noted");
	}

	// Now reset the variables so both dataCopied anmd dataPasted need to be called again
	// during the next drag 'n' drop op between Seahawk and the browser.
	lastPastedSessionId = null;
	lastCopiedSource = null;
	lastCopiedValue = null;
	lastCopiedRuleURI = null;
    }

    // Here we look at each input to the service and see if any of them are 
    // derived from a Seahawk paste operation, in which case we can define
    // the Moby datatype directly.  Also check if it's an xsd enum, in which
    // case we automatically create a moby secondary param enum. 
    // If it isn't, see if a MOB rule applies
    // and has a matching DEM rule to generate the original input.  If so, 
    // make that Moby datatype the default.  If neither applies, see if
    // a basic moby type applies (e.g. int, boolean, datetime), and give
    // the choice of whether it's a primary or secondary param.  As a last
    // resort, the user can create their own enum or it's a secondary string. 
    private String getInputChoices(HttpServletRequest request, String inElementName, String inputText){
	StringBuilder result = new StringBuilder();
	HttpSession session = request.getSession(true);
	Map<MobyDataInstance,String[]> pastedData = sessionId2PastedData.get(session.getId());

	// Map<pastedData,startPosInInputText>
	Map<MobyDataInstance,Integer> pastedLocs = new HashMap<MobyDataInstance,Integer>();
	if(pastedData != null && !pastedData.isEmpty()){
	    for(Map.Entry<MobyDataInstance,String[]> mdi: pastedData.entrySet()){
		if(mdi.getValue() == null || mdi.getValue()[0] == null){
		    continue;
		}
		int pasteLoc = inputText.indexOf(mdi.getValue()[0]); //0 is the value, 1 is the transform URI
		// note: currently we don't handle pasting the same data more than once in one field
		if(pasteLoc != -1){
		    System.err.println("Found pasted item at "+pasteLoc+" in "+inElementName);
		    pastedLocs.put(mdi.getKey(), new Integer(pasteLoc));
		}
	    }
	}

	// if the field value is derived from pasted MOBY data
	if(!pastedLocs.isEmpty()){
	    // vector literal text and moby data instance, hence no template
	    Vector valueParts = new Vector();
	    int cursor = 0;
	    // get the parts left to right, the cursor marks how far towards the right we are
	    MobyDataInstance[] mdis = new MobyDataInstance[pastedLocs.size()];
	    int i = 0;
	    for(MobyDataInstance mdi: pastedLocs.keySet()){		
		mdis[i++] = mdi;
	    }
	    // sort'em left to right
	    Arrays.sort(mdis, new MapValueComparator(pastedLocs));
	    for(i = 0; i < mdis.length; i++){
		int pasteLoc = pastedLocs.get(mdis[i]).intValue();
		if(pasteLoc > cursor){
		    //there's fixed text before the pasted moby data 
		    valueParts.add(inputText.substring(cursor,pasteLoc));
		    cursor = pasteLoc;
		}
		valueParts.add(mdis[i]);
		cursor += pastedData.get(mdis[i])[0].length();
	    }
	    // add any trailing fixed text
	    if(cursor < inputText.length()){
		valueParts.add(inputText.substring(cursor));
	    }
	    // print an interface
	    result.append(getInputChoice(request, inElementName, valueParts));
	}
	// else the type must be inferred
	else{
	    //todo: try text->mob->dem->text round-tripping

	    result.append("<td>"+getSecondaryParamOptions(request, inElementName, inputText)+"</td>");
	}
	

	return result.toString();
    }

    // Compares objects based on their corresponding values in an map
    class MapValueComparator<T extends Object> implements Comparator{
	Map<T,Comparable> map;
	public MapValueComparator(Map<T,Comparable> objToBeSorted2sortingValue){
	    map = objToBeSorted2sortingValue;
	}
	public int compare(Object o1, Object o2){
	    return map.get(o1).compareTo(map.get(o2));
	}
	public boolean equals(Object obj){
	    return super.equals(this); // use Object class equals
	}
    }

    private String getInputChoice(HttpServletRequest request, String inElementName, Vector parts){
	StringBuilder result = new StringBuilder();

	HttpSession session = request.getSession(false);
	Map<MobyDataInstance,String[]> pastedData = sessionId2PastedData.get(session.getId());
	// for the wrapping spec later, note which field is generated by which transform rule (by URI), 
	// and what the sample data is
	if(!sessionId2FieldTransformMap.containsKey(session.getId())){
	    sessionId2FieldTransformMap.put(session.getId(), new HashMap<String,String>());
	    sessionId2MobySrcs.put(session.getId(), new HashMap<String,MobyDataInstance>());
	}
	Map<String,String> fieldName2TransformRule = sessionId2FieldTransformMap.get(session.getId());
	Map<String,MobyDataInstance> fieldName2MobySrc = sessionId2MobySrcs.get(session.getId());

	int partCount = 0;
	for(Object part: parts){
	    String fieldName = inElementName+"-"+(partCount++);
	    String partValue = getParam(request, fieldName);
	    result.append("<td>");
	    if(part instanceof MobyDataInstance){

		String transformRuleURI = pastedData.get(part)[1];
		fieldName2TransformRule.put(fieldName, transformRuleURI);
		fieldName2MobySrc.put(fieldName, (MobyDataInstance) part);

		result.append("<select name=\""+fieldName+"\">");
		if(part instanceof MobyDataObject){
		    MobyDataObject obj = (MobyDataObject) part;
		    MobyDataType[] lineage = obj.getDataType().getLineage();
		    // todo: check against TextClient's knowledge of required fields
		    // go from most specific to least
		    for(int i = lineage.length-1; i >= 0; i--){
			if(i == 0){
			    MobyNamespace ns = obj.getPrimaryNamespace();
			    if(ns != null){
				result.append("<option title=\""+getContext(ns.getDescription(), "", 50, false)+
					      (partValue.equals(MobyTags.MOBYOBJECT+":"+ns.getName()) ? "\" selected=\"selected" : "") +
					      "\">"+MobyTags.MOBYOBJECT+":"+ns.getName()+"</option>");
			    }
			}
			else{
			    result.append("<option title=\""+getContext(lineage[i].getDescription(),"", 50, false)+
					  (partValue.equals(lineage[i].getName()) ? "\" selected=\"selected" : "") +
					  "\">"+lineage[i].getName()+"</option>");
			}
		    }
		    result.append("</select>");
		}
		else if(part instanceof MobyDataSecondaryInstance){
		    //todo
		    result.append("</select>");
		}
	    }
	    else{
		result.append(getSecondaryParamOptions(request, fieldName, part.toString()));
		fieldName2MobySrc.put(fieldName, null); // not a Moby source for the value
	    }
	    result.append("</td>");
	}

	return result.toString();
    }

    // Given an input string, determine which, if any, Moby secondary input type it could be.
    // returns an HTML select's option tags, or blank if none apply.
    private String getSecondaryParamOptions(HttpServletRequest request, String fieldName, String literalValue){
	StringBuilder result = new StringBuilder();
	String fieldValue = getParam(request, fieldName);  // in the form, usually a Moby spec, not the literal value

	result.append("<table><tr><td colspan=\"2\" align=\"center\"><select name=\""+
		      fieldName+"\" onChange=\"this.form.submit()\">");
	result.append("<option title=\"A literal value, does not change "+
		      "between service calls\">\""+literalValue+"\"</option>\n");
	// allow the user to constrain the value
	result.append("<option "+(fieldValue.equals(CUSTOM_INPUT_PART_OPTION) ? "selected=\"selected\"" : "")+
		      ">"+CUSTOM_INPUT_PART_OPTION+"</option>\n");
	try{
	    MobyDataFloat mf = new MobyDataFloat("test", literalValue);
	    result.append("<option "+(fieldValue.equals(FLOAT_INPUT_PART_OPTION) ? "selected=\"selected\"" : "")+
			  ">"+FLOAT_INPUT_PART_OPTION+"</option>\n");
	} catch(NumberFormatException nfe){}
	try{
	    MobyDataInt mi = new MobyDataInt("test", literalValue);
	    result.append("<option "+(fieldValue.equals(INT_INPUT_PART_OPTION) ? "selected=\"selected\"" : "")+
			  ">"+INT_INPUT_PART_OPTION+"</option>\n");
	} catch(NumberFormatException nfe){}
	try{
	    MobyDataDateTime mi = new MobyDataDateTime("test", literalValue);
	    result.append("<option "+(fieldValue.equals(DATE_INPUT_PART_OPTION) ? "selected=\"selected\"" : "")+
			  ">"+DATE_INPUT_PART_OPTION+"</option>\n");
	} catch(IllegalArgumentException iae){}
	result.append("</select></td></tr>");

	// user just selected to create a custom list (string enumeration)
	if(CUSTOM_INPUT_PART_OPTION.equals(fieldValue)){
	    String customValue = getParam(request, fieldName+CUSTOM_INPUT_PART_VALUES_SUFFIX).trim();
	    if(customValue.length() == 0){
		customValue = "value1\nvalue2\n...";
	    }
	    result.append("<tr><td colspan=\"2\"><textarea name=\""+fieldName+CUSTOM_INPUT_PART_VALUES_SUFFIX+
			  "\" cols=\"20\" rows=\"4\" title=\"New-line separated list of possible values\">"+
			  customValue+"</textarea></td></tr></table>");
	}
	else if(FLOAT_INPUT_PART_OPTION.equals(fieldValue) ||
		INT_INPUT_PART_OPTION.equals(fieldValue) ||
		DATE_INPUT_PART_OPTION.equals(fieldValue)){
	    String minValue = getParam(request, fieldName+SCALAR_INPUT_PART_MIN_SUFFIX);
	    String maxValue = getParam(request, fieldName+SCALAR_INPUT_PART_MAX_SUFFIX);

	    //todo: check format of min/max values
	    result.append("<tr><td>Min:<input name=\""+fieldName+SCALAR_INPUT_PART_MIN_SUFFIX+
			  "\" value=\""+minValue+"\" size=\"5\" title=\"Lower bound is optional\"/></td>");
	    result.append("<td>Max:<input name=\""+fieldName+SCALAR_INPUT_PART_MAX_SUFFIX+
			  "\" value=\""+maxValue+"\" size=\"5\" title=\"Upper bound is optional\"/></td></tr></table>");
	}
	else{
	    result.append("</table>");
	}

	return result.toString();
    }

    private String getInputFieldName(Element e, int count){
	return getInputFieldName(e)+"-"+count;
    }

    private String getInputFieldName(Element e){
	// prepend element nesting info in payload, except root element
	// allows for easy mapping back to SourceMap later
	String prefix = "";
	for(Element parent = (Element) e.getParentNode(); 
	    parent != null && parent.getParentNode() != null && parent.getParentNode() instanceof Element; 
	    parent = (Element) parent.getParentNode()){
	    prefix += parent.getNodeName()+":";
	}
	return prefix+e.getNodeName();
    }

    private String wrappingMetadata(HttpServletRequest request, Map<String,String> todo){
	StringBuilder result = new StringBuilder();

	result.append("<h3>Metadata (all fields must be completed)</h3>\n");

	result.append("Service name (alphanumeric only, no spaces): <input name=\""+
		      Registration.MOBY_SERVICENAME_PARAM+
		      "\" value=\""+getParam(request, Registration.MOBY_SERVICENAME_PARAM)+"\"/>"+
		      getErr(todo, Registration.MOBY_SERVICENAME_PARAM)+"<br>\n");

	result.append("Service type: <input name=\""+Registration.MOBY_SERVICETYPE_PARAM+
		      "\" size=\"50\" id=\""+
		      MOBY_SERVICETYPE_FIELD_ID+"\" ");
	result.append("type=\"text\" autocomplete=\"off\" value=\""+
		      getParam(request, Registration.MOBY_SERVICETYPE_PARAM)+"\"/>"+
		      getErr(todo, Registration.MOBY_SERVICETYPE_PARAM) + "<br/>\n");
	result.append("<input type=\"hidden\" id=\""+MOBY_SERVICETYPE_FIELD_ID+"hidden\"/>\n");
	result.append("<div id=\""+MOBY_SERVICETYPE_FIELD_ID+"Complete\" class=\"MobyCompleteBox\" " +
		      "style=\"visibility:hidden;display:block;\"></div>\n");


	result.append("Your e-mail (for administrative purposes): <input name=\""+Registration.MOBY_AUTHOR_PARAM+"\" value=\""+
		      getParam(request, Registration.MOBY_AUTHOR_PARAM)+"\"/>" + 
		      getErr(todo, Registration.MOBY_AUTHOR_PARAM) + "<br/>\n");

	result.append("Textual description of what your new service does (please be specific!):"+
		      getErr(todo, Registration.MOBY_DESC_PARAM) + "<br/>\n");
	result.append("<textarea cols=\"50\" rows=\"4\" name=\""+Registration.MOBY_DESC_PARAM+"\">"+
		      getParam(request,Registration.MOBY_DESC_PARAM)+"</textarea><br>\n");

	result.append("<input type=\"submit\" name=\""+MOBY_CREATE_PARAM+"\" value=\"Create the Moby Service!\"/>\n");

	return result.toString();
    }

    private String wrappingFooter(HttpServletRequest request, Map<String,String> todo){
	return "</form></body></html>";
    }

    // prints a message in the case the given CGI field is not correct according to validateForm
    private String getErr(Map<String,String> todoText, String fieldName){
	if(todoText.containsKey(fieldName)){
	    return "<nobr><font color=\"red\" style=\"bold\">"+todoText.get(fieldName)+"</font></nobr>";
	}
	else{
	    return "";
	}
    }

    // echoes the CGI value submitted, if any exists, otherwise returns a blank rather than "null"
    private String getParam(HttpServletRequest request, String paramName){
	String val = request.getParameter(paramName);
	if(val == null){
	    val = "";
	}

	return val.replaceAll("<[^>]+>","");
    }

    // variables passed around inside the servlet, to be used within the timeframe of a response to the request,
    // rather than passing them around to each function explicitly
    private String getInternalParam(HttpServletRequest request, String paramName){
	if(!params.containsKey(request)){
	    return null;
	}
	return params.get(request).get(paramName);
    }

    private void clearInternalParams(HttpServletRequest request){
	params.remove(request);
    }

    private void setInternalParam(HttpServletRequest request, String paramName, String value){
	if(!params.containsKey(request)){
	    params.put(request, new HashMap<String,String>());
	}
	params.get(request).put(paramName, value);
    }
}
