package ca.ucalgary.seahawk.util;

import org.biomoby.client.CentralImpl;
import org.biomoby.client.MobyRequestEvent;
import org.biomoby.registry.meta.*;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;

import org.w3c.dom.*;

import javax.xml.parsers.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.*;

import java.io.*;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.*;

/**
 * Takes a browsing history in Seahawk and turns it into a Taverna2 workflow (Programming-by-Example).
 */

public class DataFlowRecorder{
    public static final int TAVERNA15 = 167;
    public static final int T2FLOW = 169;
    public static final String T2FLOW_NS = "http://taverna.sf.net/2008/xml/t2flow";
    public static final String T2FLOW_DISPATCHXML = "ca/ucalgary/seahawk/resources/t2flowDispatchStack.xml";
    public static final String T2FLOW_REGEXFILTER_BEANSHELL = "ca/ucalgary/seahawk/resources/RegexFilterBeanShell";
    public static final String T2FLOW_XPATHFILTER_BEANSHELL = "ca/ucalgary/seahawk/resources/XPathFilterBeanShell";
    public static final String T2FLOW_PASSFILTER_BEANSHELL = "ca/ucalgary/seahawk/resources/PassFilterBeanShell"; // = if condition
    public static final String T2FLOW_LISTFLATTEN_BEANSHELL = "ca/ucalgary/seahawk/resources/FlattenListBeanShell";

    private Central mobyCentral;
    private Map<String,Integer> namesUsed;  // keep count of workflow element name usage so as not to duplicate an ID
    private Map<String,String> url2Processor;  // keep track of processor usage so as not to duplicate a processor when the workflow has a fork
    // track filters so we don't duplicate when a workflow forks and the condition isn't changed for each branch.  
    // Value is processor and port names
    private Map<String,String[]> filter2Processor;
    private Map<String,String[]> decomp2Processor; // reuse decomposition processors if same decomp done more than once on a given doc
    private Map<String,String[]> input2Processor; // if same input use more than once, create only one input port
    private DocumentBuilder docBuilder;
    private static String regexFilterScript = null;
    private static String xpathFilterScript = null;
    private static String passFilterScript = null;
    private static String listFlattenScript = null;
    private static Element dispatchStack = null;
    private static Transformer nullTransformer = null;
    private static Logger logger = Logger.getLogger(DataFlowRecorder.class.getName());

    public DataFlowRecorder(Central mobyCtr) throws Exception{
	if(mobyCtr == null){
	    throw new IllegalArgumentException("Moby Central object passed to DataFlowRecorder was null");
	}
	mobyCentral = mobyCtr;
	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);
	docBuilder = dbf.newDocumentBuilder();

	namesUsed = new HashMap<String,Integer>();
	url2Processor = new HashMap<String,String>();
	filter2Processor = new HashMap<String,String[]>(); // key is compound:  url \n regex \n xpath \n caseSensitivity
	decomp2Processor = new HashMap<String,String[]>(); // key is url with xpath attached as ref (the way Seahawk generates them)
	input2Processor = new HashMap<String,String[]>(); // key is input instance XML 
    }

    /**
     * Given a MOBY document, traces back the workflow required to create it.
     * 
     * @param resultDocuments the output of the workflow to backtrace
     * @param outputStream where to write the output
     * @param workflowName to be used as a label in the workflow
     * @param format the format to write the workflow in (currently, only DataFlowRecorder.T2FLOW works)
     */
    public void exportWorkflow(Map<URL,FilterSearch> resultDocuments, OutputStream outputStream, String workflowName, String workflowTitle, String workflowDescription, String workflowAuthor, int format) throws Exception{
	if(format != TAVERNA15 && format != T2FLOW){
	    throw new IllegalArgumentException("The export format for the workflow was " +
					       "not DataFlowRecorder.TAVERNA15 nor DataFlowRecorder.T2FLOW as expected, found " + format);
	}
	if(outputStream == null){
	    throw new IllegalArgumentException("The output stream for workflow exporting was null, aborting export");
	}
	if(resultDocuments == null){
	    throw new IllegalArgumentException("The URL endpoint to export was null, aborting workflow export");
	}

	if(format == T2FLOW){
	    exportT2Flow(resultDocuments, outputStream, workflowName, 
			 workflowTitle, workflowDescription, workflowAuthor);
	}
	else{
	    throw new IllegalArgumentException("You asked for SCUFL. Only T2Flow-formatted export of workflows " +
					       "is currently supported, sorry!");
	}
    }

    /**
     * Turn a list of result files from Seahawk into a Taverna 2.0+ formatted workflow
     * by backtracking from the results to the services that created them, repeating until
     * in the inputs to services were literal values given to Seahawk.
     *
     * @param resultDocs the list of example documents that should be produced as output from the workflow
     * @param outputStream where to write the resulting T2Flow document
     * @param workflowName will be used to title the workflow
     */
    private synchronized void exportT2Flow(Map<URL,FilterSearch> resultDocs, OutputStream outputStream, String workflowName, String workflowTitle, String workflowDescription, String workflowAuthor) 
	throws Exception{
	Document doc = docBuilder.newDocument();
	Element root = doc.createElementNS(T2FLOW_NS, "workflow");
	doc.appendChild(root);
	root.setAttribute("version", "1");
	root.setAttribute("producedBy", "Seahawk 1.0");

	Element dataflow = doc.createElementNS(T2FLOW_NS, "dataflow");
	dataflow.setAttribute("id", java.util.UUID.randomUUID().toString());
	dataflow.setAttribute("role", "top");
	root.appendChild(dataflow);

	Element name = doc.createElementNS(T2FLOW_NS, "name");
	name.appendChild(doc.createTextNode(workflowName));
	dataflow.appendChild(name);

	// set up the main sections, details to be filled in later
	Element inputPorts = doc.createElementNS(T2FLOW_NS, "inputPorts");
	dataflow.appendChild(inputPorts);
	Element outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	dataflow.appendChild(outputPorts);
	Element processors = doc.createElementNS(T2FLOW_NS, "processors");
	dataflow.appendChild(processors);

	// no "conditions" tag applicable yet, leaving empty
	dataflow.appendChild(doc.createElementNS(T2FLOW_NS, "conditions"));

	Element datalinks = doc.createElementNS(T2FLOW_NS, "datalinks");
	dataflow.appendChild(datalinks);

	Element annotations = doc.createElementNS(T2FLOW_NS, "annotations");
	dataflow.appendChild(annotations);
	if(workflowAuthor == null || workflowAuthor.trim().length() == 0){
	    // glean user info from environment if not otherwise provided
	    workflowAuthor = "Initially created using Seahawk by " + System.getProperty("user.name") + " @ " +
		java.net.InetAddress.getLocalHost().getCanonicalHostName() + " on " +
		dateAsText();
	}
	annotations.appendChild(createAnnotationChain("net.sf.taverna.t2.annotation.annotationbeans.Author", workflowAuthor, doc));
	if(workflowDescription != null && workflowDescription.trim().length() != 0){
	    annotations.appendChild(createAnnotationChain("net.sf.taverna.t2.annotation.annotationbeans.FreeTextDescription", 
							  workflowDescription, doc));
	    
	}
	if(workflowTitle != null && workflowTitle.trim().length() != 0){
	    annotations.appendChild(createAnnotationChain("net.sf.taverna.t2.annotation.annotationbeans.DescriptiveTitle", 
							  workflowTitle, doc));
	}

	namesUsed.clear();  // internal Taverna label table
	url2Processor.clear();  // clear map of results already produced in backtracking (handling workflow forks)
	filter2Processor.clear(); // ditto
	decomp2Processor.clear(); // ditto
	input2Processor.clear(); // ditto

	// Generate all of the outputs requested by the call.  None should be intermediaries 
	// of another (the backtracking done by addWorkflowElements will capture these).
	//        for(int i = 0; i < resultDocs.length; i++){
	for(Map.Entry<URL,FilterSearch> result: resultDocs.entrySet()){ 
	    // Create the inputs, processors and data links required for the workflow to create the result doc
	    String ultimateProcessorName = addWorkflowElements(result.getKey(), doc, inputPorts, processors, datalinks);
	    
	    // If there is a filter active on the currently displayed doc, we need to apply it here
	    FilterSearch filter = result.getValue();
	    String[] regexProcessorAndPorts = null;
	    String filterKey = null;
	    if(filter != null && filter.getFilterRegex().length() > 0){
		filterKey = result.getKey()+"\n"+filter.getFilterRegex()+"\n"+
		    filter.getSelectedXPath().getXPath()+"\n"+filter.getCaseSensitivity();
	    }

	    // Create the workflow output and data links
	    MobyService service = DataUtils.getService(result.getKey());
	    for(MobyPrimaryData outputParam: service.getPrimaryOutputs()){
		String outputName = outputParam.getName();
		String outputType = outputParam.getDataType().getName();
		if(outputType.equals(MobyTags.MOBYOBJECT)){  //base Object, so use namespace instead 
		    MobyNamespace[] nss = outputParam.getNamespaces();
		    if(outputParam.getDataType().getName().equals(MobyTags.MOBYOBJECT) &&
		       nss != null && nss.length > 0){
			outputType = nss[0].getName();
		    }
		}
		String uniqueOutputName = createUniqueName(outputType+"-"+outputName);
		outputPorts.appendChild(createWorkflowOutputElement(uniqueOutputName, doc));
		if(filterKey != null){
		    if(filter2Processor.containsKey(filterKey)){
			regexProcessorAndPorts = filter2Processor.get(filterKey);
		    }
		    else{
			regexProcessorAndPorts = createRegexFilter(filter.getFilterRegex().toString(), 
								   filter.getSelectedXPath(),
								   filter.getCaseSensitivity(),
								   ultimateProcessorName,
								   getPortName(outputParam, true),
								   1, // desired list dpeth
								   processors, datalinks, doc);
			filter2Processor.put(filterKey, regexProcessorAndPorts);
		    }

		    // The proc has an output port for non-matches (penultimate index) and matches (ultimate index)
		    int regexOutPort = regexProcessorAndPorts.length-(filter.getSelectionInversed() ? 2 : 1);
		    datalinks.appendChild(createWorkflowOutputLinkElement(regexProcessorAndPorts[0],
									  regexProcessorAndPorts[regexOutPort],
									  uniqueOutputName,
									  doc));
		    break; // only one output when filter is attached: the XML doc
		}
		else{
		    datalinks.appendChild(createWorkflowOutputLinkElement(ultimateProcessorName,
									  getPortName(outputParam, outputParam instanceof MobyPrimaryDataSet), 
									  uniqueOutputName, 
									  doc));
		}
	    }
	}

	// Serialize the DOM
	logger.log(Level.INFO, "Sending workflow XML to output stream " + outputStream);
	//getTransformer().transform(new DOMSource(root), new StreamResult(System.err));
	getTransformer().transform(new DOMSource(root), new StreamResult(outputStream));
    }

    private Element createWorkflowOutputElement(String name, Document doc){
	Element portEl = doc.createElementNS(T2FLOW_NS, "port");
	Element nameEl = doc.createElementNS(T2FLOW_NS, "name");
	nameEl.appendChild(doc.createTextNode(name));
	portEl.appendChild(nameEl);
	return portEl;
    }

    private Element createWorkflowInputElement(String name, MobyPrimaryData sampleData, Document doc){
	Element portEl = doc.createElementNS(T2FLOW_NS, "port");

	Element nameEl = doc.createElementNS(T2FLOW_NS, "name");
	nameEl.appendChild(doc.createTextNode(name));
	portEl.appendChild(nameEl);

	Element depthEl = doc.createElementNS(T2FLOW_NS, "depth");
	depthEl.appendChild(doc.createTextNode("0"));
	portEl.appendChild(depthEl);

	Element granularDepthEl = doc.createElementNS(T2FLOW_NS, "granularDepth");
	granularDepthEl.appendChild(doc.createTextNode("0"));
	portEl.appendChild(granularDepthEl);

	MobyNamespace ns = null;
	if(sampleData.getNamespaces() != null &&
	   sampleData.getNamespaces().length > 0){
	    ns = sampleData.getNamespaces()[0];
	}

	String descText = "File of one or more"+(ns == null ? "" : " "+
						 ns.getName())+" IDs, one per line";
	String exampleText = sampleData.getId();
	Element annotations = doc.createElementNS(T2FLOW_NS, "annotations");
	portEl.appendChild(annotations);
	annotations.appendChild(createAnnotationChain("net.sf.taverna.t2.annotation.annotationbeans.FreeTextDescription", descText, doc));
	annotations.appendChild(createAnnotationChain("net.sf.taverna.t2.annotation.annotationbeans.ExampleValue", exampleText, doc));
	
	return portEl;
    }

    // For a given Moby XML result (final or intermediate), add the T2Flow xml 
    // elements required to capture its generation.
    // The returned value is the processor name for the service (useful for creating data links during backtracking) 
    private String addWorkflowElements(URL resultURL, Document doc,
				       Element inputPorts, Element processors, Element datalinks) throws Exception{
        Document resultDom = null;
	try{
	    resultDom = docBuilder.parse(resultURL.openStream());
	} catch(org.xml.sax.SAXException saxe){
	    throw new Exception("The XML data could not be parsed (not well-formed): " + saxe);
	} catch(java.io.IOException ioe){
	    throw new Exception("The XML data could not be loaded (I/O problem): " + ioe);
	}

	MobyService service = DataUtils.getService(resultDom);
	Registry registry = DataUtils.getRegistry(resultDom);
	MobyDataJob sampleJob = DataUtils.getInputSample(resultDom, registry);
	if(sampleJob == null){
	    return "foo";  //todo: doc was a loaded Moby XML, not a service output or recognized data
	}

	Element processorElement = createProcessorElement(service, sampleJob, doc);
	// add processor to workflow
	processors.appendChild(processorElement);
	String processorName = processorElement.getFirstChild().getTextContent(); //gets XPath "processor/name/text()"
	String resultURLString = resultURL.toString().replaceFirst("#.*$", ""); //get rid of #ref bit, if any exists
	logger.log(Level.INFO, "Adding processor for " + resultURLString);
	url2Processor.put(resultURLString, processorName);

	// Trace the inputs to either the outputs of other preceding services, or user input. 
	// i.e. link up the inputs for the processor to their sources
	for(String inputName: sampleJob.keySet()){
	    MobyDataInstance sd = sampleJob.get(inputName);

	    if(sd instanceof MobySecondaryData){
		// secondary parameters were handled by createProcessorElement()
		continue;
	    }

	    MobyPrimaryData sampleData = (MobyPrimaryData) sd;

	    // has provenance info?
	    String[] data = null;
	    String[] condPassProcessorAndPorts = null;  //[procname, input port, outputport]
	    if(sampleData.getUserData() != null){
		// User data has the form 
		// srcURL#generalSelectionXPath <tab> actualInputDataXptr <tab> regexFilter <tab> conditionalURL
		// where the filter and conditionalURL are optional 
		data = sampleData.getUserData().toString().split("\t");
		//options: selection + filter + cond, selection + cond, or cond only
		if(data.length == 14 || data.length == 8 || data.length == 6){
		    //		    System.err.println("Adding conditional for " + resultURLString);
		    String conditionURL = data[data.length-6];
		    String conditionRegex = data[data.length-5];
		    XPathOption conditionXPath = new XPathOption(data[data.length-4], data[data.length-3]);
		    boolean caseSensitivity = Boolean.parseBoolean(data[data.length-2]);
		    boolean inverse = Boolean.parseBoolean(data[data.length-1]);
		    condPassProcessorAndPorts = createServiceConditionFilter(new URL(conditionURL), conditionRegex, 
									     conditionXPath, caseSensitivity, inverse,
									     doc, inputPorts, processors, datalinks);
		    String[] conditionlessData = new String[data.length-6];
		    System.arraycopy(data, 0, conditionlessData, 0, data.length-6);
		    data = conditionlessData;
		}
// 		else{
// 		    System.err.println("Skipping conditional, only " + data.length + 
// 				       "members in provenance data for " + resultURLString);
// 		}
	    }

	    // true means treat as a collection if that's what the sample data is
	    String sinkPortName = getPortName(sampleData, true);
	    // No user data means non-moby origin...backtracking stops.
	    if(data != null && data.length != 0){
		URL dataSrcURL = null;
		try{
		    dataSrcURL = new URL(data[0]);
		} catch(Exception e){
		    logger.log(Level.WARNING, "Ignoring unexpected UserData in Moby DOM, was not " +
			       "a provenance URL as expected URL (" + sampleData.getUserData().toString() + ")");
		    continue;
		}

		// If srcService == null, the input source for this param was a Moby doc that wasn't a 
		// service output, e.g. the clipboard or an old doc loaded from file.
		MobyService srcService = DataUtils.getService(dataSrcURL);
		if(srcService == null){
		    addWorkflowInput(processorName, condPassProcessorAndPorts, sinkPortName, sampleData, 
				     datalinks, processors, inputPorts, doc);
		    continue;
		}

		// recursion for workflow creation by backtracking service input provenance
		String feedingProcessorName = null;
		String feedingProcessorPort = null;
		String dataSrcURLString = dataSrcURL.toString().replaceFirst("#.*$", ""); //get rid of ref part
		if(url2Processor.containsKey(dataSrcURLString)){
		    feedingProcessorName = url2Processor.get(dataSrcURLString);
		}
		else{
		    feedingProcessorName = addWorkflowElements(dataSrcURL, doc, inputPorts, processors, datalinks);
		}
		
		// Take into account data[2..6] if they are present, 
		// which filter the data by a regex before any other activities happen
		// Format of spec is regex <tab> xpath <tab> xpathTextDesc <tab> booleanForCaseSensitivity
		if(data.length == 7){
		    String[] origFeederProcessorAndPort = getPortFromURLRef(dataSrcURL,
									    sampleData,
									    feedingProcessorName,
									    null,
									    processors, 
									    datalinks,
									    doc,
									    false);
		    
		    // Lookup key is url \n regex \n xpath \n caseSensitivity 
		    // (inverse selection uses same processor, different output port, so not part of the key)
		    String[] regexProcessorAndPorts = null;
		    String filterKey = dataSrcURLString+"\n"+data[2]+"\n"+data[3]+"\n"+data[5];
		    boolean inversed = Boolean.parseBoolean(data[6]);
		    if(filter2Processor.containsKey(filterKey)){ // filter already exists from another branch
			regexProcessorAndPorts = filter2Processor.get(filterKey);
		    }
		    else{
			regexProcessorAndPorts = createRegexFilter(data[2], 
								   new XPathOption(data[3], data[4]),
								   Boolean.parseBoolean(data[5]),
								   origFeederProcessorAndPort[0], 
								   origFeederProcessorAndPort[1],
								   1, // desired list depth 
								   processors, datalinks, doc);
			// New filter is applied to just this branch from a service for the moment...record the filter
			// so that if same filter criteria are applied to more than one branch, we only make one filter
			filter2Processor.put(filterKey, regexProcessorAndPorts);
		    }
		    feedingProcessorName = regexProcessorAndPorts[0];
		    // The proc has an output port for non-matches (penultimate index) and matches (ultimate index)
		    int regexOutPort = regexProcessorAndPorts.length-(inversed ? 2 : 1);
		    feedingProcessorPort = regexProcessorAndPorts[regexOutPort];  
		}
		
		// getPortFromURLRef() may inject extra processors between the processorName and feedingProcessorName
		// in order to maintain type safety, etc. so the data link may change (based on last arg being set to true).
		String[] feederProcessorAndPort = getPortFromURLRef(dataSrcURL,
								    sampleData,
								    feedingProcessorName,
								    feedingProcessorPort, //if null, determined from urlref
								    processors, 
								    datalinks,
								    doc,
								    true); 
		if(condPassProcessorAndPorts != null){
		    // inject condition filter between feeder service and current service 
		    datalinks.appendChild(createDataLinkElement(feederProcessorAndPort[0],
								feederProcessorAndPort[1],
								condPassProcessorAndPorts[0], 
								condPassProcessorAndPorts[1],
								doc));
		    feederProcessorAndPort[0] = condPassProcessorAndPorts[0];
		    feederProcessorAndPort[1] = condPassProcessorAndPorts[2];
		}

		datalinks.appendChild(createDataLinkElement(feederProcessorAndPort[0], feederProcessorAndPort[1],
							    processorName, sinkPortName, 
							    doc));
		
	    }
	    // Otherwise it's a workflow input the user will need to specify
	    else{
		addWorkflowInput(processorName, condPassProcessorAndPorts, sinkPortName, sampleData, 
				 datalinks, processors, inputPorts, doc);
	    }
	}

	return processorName;
    }

    private void addWorkflowInput(String pName, String[] condPassProcessorAndPorts, String sinkPortName, 
				  MobyPrimaryData sampleData, Element datalinks, Element processors, 
				  Element inputPorts, Document doc) throws Exception{
	// pName is editable name for input processor target...
	// allows injection of condition without affecting method's return value
	if(condPassProcessorAndPorts != null){
	    // inject condition filter between data creator and current service 
	    datalinks.appendChild(createDataLinkElement(condPassProcessorAndPorts[0],
							condPassProcessorAndPorts[2],
							pName,
							sinkPortName,
							doc));
	    pName = condPassProcessorAndPorts[0];
	    sinkPortName = condPassProcessorAndPorts[1];
	}
	// The sample data may be used in more than one branch of the flow...avoid duplication
	// Get the real data XML, to see if we've encountered it before as input (it'd be in the hash)
	// Remember that sd is the loop's original MobyDataInstance before it's been coerced into a MobyPrimaryData, 
	// which doesn't have XML modes, etc.
	String inputKey = getInputKey((MobyDataInstance) sampleData);
	//		System.err.println("Input key for " + processorName + " is " + inputKey);
	
	String[] mobifyingProcessorNameAndPorts = null;
	if(input2Processor.containsKey(inputKey)){
	    mobifyingProcessorNameAndPorts = input2Processor.get(inputKey);		    
	}
	else{
	    if(sampleData.getDataType().getName().equals(MobyTags.MOBYOBJECT)){
		mobifyingProcessorNameAndPorts = addIdMobifyingProcessor(processors, datalinks, inputPorts,
									 sampleData, doc);
		input2Processor.put(inputKey, mobifyingProcessorNameAndPorts);
	    }
	    else{
		// TODO: Need to build complex input from MOB rule or spreadsheet fields?
	    }
	}
	// link the created data to the workflow service
	datalinks.appendChild(createDataLinkElement(mobifyingProcessorNameAndPorts[0], 
						    mobifyingProcessorNameAndPorts[1], 
						    pName, sinkPortName, 
						    doc));	
    }

    // returns the same value for moby sample data instances containing the same data in XML form
    private String getInputKey(MobyDataInstance sd){
	int oldXmlMode = sd.getXmlMode();
	sd.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
	String inputKey = sd.toXML();
	sd.setXmlMode(oldXmlMode);

	// get rid of the top-level article name, as this is immaterial to the 
	// value equivalence of objects (only matters to the actual service call mechanism)
	return inputKey.replaceFirst(MobyTags.ARTICLENAME+"\\s*=\\s*(['\"]).*?\\1", "");
    }

    // Actually adds a few processors, but only one needs to be returned and connected to an input port.
    // The others are constant values.  By default we assume that you want to run a bunch of IDs 
    // in a file, we also link in a Taverna spreadsheet importer.
    // Returns [spreadsheetReadingProcName, outputPort]
    private String[] addIdMobifyingProcessor(Element processors, Element datalinks, Element dataFlowInputPorts,
					     MobyPrimaryData sampleData, Document doc) throws Exception{
	String processorName = "Create-" + sampleData.getDataType().getName();
	MobyNamespace ns = null;
	MobyNamespace[] nss = ((MobyPrimaryData) sampleData).getNamespaces();
	if(sampleData.getDataType().getName().equals(MobyTags.MOBYOBJECT) &&
	   nss != null && nss.length > 0){
	    ns = nss[0];  //we will use only the primary namespace
	    processorName = "Create-" + ns.getName() + "-ID";
	}
	processorName = createUniqueName(processorName);

	Element processor = doc.createElementNS(T2FLOW_NS, "processor");
	processors.appendChild(processor);
	Element name = doc.createElementNS(T2FLOW_NS, "name");
	name.appendChild(doc.createTextNode(processorName));
	processor.appendChild(name);

	Element inputPorts = doc.createElementNS(T2FLOW_NS, "inputPorts");
	processor.appendChild(inputPorts);
	Element outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	processor.appendChild(outputPorts);
	
	Element annotations = doc.createElementNS(T2FLOW_NS, "annotations");
	processor.appendChild(annotations);
	Element activities = doc.createElementNS(T2FLOW_NS, "activities");
	processor.appendChild(activities);
	Element activity = doc.createElementNS(T2FLOW_NS, "activity");
	activities.appendChild(activity);
	Element raven = doc.createElementNS(T2FLOW_NS, "raven");
	Element group = doc.createElementNS(T2FLOW_NS, "group");
	group.appendChild(doc.createTextNode("net.sf.taverna.t2.activities"));
	raven.appendChild(group);
	Element artifact = doc.createElementNS(T2FLOW_NS, "artifact");
	artifact.appendChild(doc.createTextNode("biomoby-activity"));
	raven.appendChild(artifact);
	Element version = doc.createElementNS(T2FLOW_NS, "version");
	version.appendChild(doc.createTextNode("1.0"));
	raven.appendChild(version);
	activity.appendChild(raven);

	Element clas = doc.createElementNS(T2FLOW_NS, "class");
	activity.appendChild(clas);
	clas.appendChild(doc.createTextNode("net.sf.taverna.t2.activities.biomoby.BiomobyObjectActivity"));

	Element inputMap = doc.createElementNS(T2FLOW_NS, "inputMap");
	activity.appendChild(inputMap);
	Element outputMap = doc.createElementNS(T2FLOW_NS, "outputMap");
	activity.appendChild(outputMap);
   
	Element configBean = doc.createElementNS(T2FLOW_NS, "configBean");
	configBean.setAttribute("encoding", "xstream");
	activity.appendChild(configBean);
	Element BiomobyObjectActivityConfigurationBean = 
	    doc.createElementNS("", 
				"net.sf.taverna.t2.activities.biomoby.BiomobyObjectActivityConfigurationBean");
	configBean.appendChild(BiomobyObjectActivityConfigurationBean);

	Element mobyEndpoint = doc.createElementNS("", "mobyEndpoint");
	String central = null;
	if(sampleData.getDataType().getRegistry() != null){
	    central = sampleData.getDataType().getRegistry().getEndpoint();
	}
	if(central == null){ // must be the default registry in this case
	    if(mobyCentral != null){
		central = mobyCentral.getRegistryEndpoint();
	    }
	    else{
		central = (new RegistriesList()).get(Registries.DEFAULT_REGISTRY_SYNONYM).getEndpoint();	    
	    }
	}
	mobyEndpoint.appendChild(doc.createTextNode(central == null ? "" : central));
	BiomobyObjectActivityConfigurationBean.appendChild(mobyEndpoint);
	Element serviceName = doc.createElementNS("", "serviceName");	
	serviceName.appendChild(doc.createTextNode(sampleData.getDataType().getName()));
	BiomobyObjectActivityConfigurationBean.appendChild(serviceName);
	BiomobyObjectActivityConfigurationBean.appendChild(doc.createElementNS("", "authorityName"));

	activity.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));
	// boiler-plate for all processors
	processor.appendChild(getDispatchStack(doc));
	Element iterationStrategyStack = doc.createElementNS(T2FLOW_NS, "iterationStrategyStack");
	processor.appendChild(iterationStrategyStack);
	Element iteration = doc.createElementNS(T2FLOW_NS, "iteration");
	iterationStrategyStack.appendChild(iteration);
	Element strategy = doc.createElementNS(T2FLOW_NS, "strategy");
	iteration.appendChild(strategy);
	Element cross = doc.createElementNS(T2FLOW_NS, "cross");
	strategy.appendChild(cross);

	// 3 input ports, one output
	String[] portNames = new String[]{"namespace", "article name", "id", "mobyData"};
	for(String portNameString: portNames){
	    // Map them
	    Element map = createMapFromTo(portNameString, portNameString, doc); //identity map
	    if(portNameString.equals("mobyData")){
		outputPorts.appendChild(createParamPort(portNameString, "0", "0", doc));
		outputMap.appendChild(map);
	    }
	    else{
		inputPorts.appendChild(createParamPort(portNameString, "0", doc));
		
		inputMap.appendChild(map);
	    
		// Add the iteration strategy
		Element portRef = doc.createElementNS(T2FLOW_NS, "port");
		portRef.setAttribute("name", portNameString);
		portRef.setAttribute("depth", "0");
		cross.appendChild(portRef);
	    }
	}

	// Create the constant value processors for the articlename 
	// NOTE: WE REPURPOSE THE VARIABLES ABOVE FOR NEW ELEMENTS!!!
	Map<String,String> constants = new HashMap<String,String>();
	Map<String,String> feeds = new HashMap<String,String>();  //what port on data creator it maps to
	constants.put(ns.getName()+"-namespace-constant", ns.getName());
	feeds.put(ns.getName()+"-namespace-constant", "namespace");
	constants.put("article_name-constant", "unimportant");
	feeds.put("article_name-constant", "article name");

	for(Map.Entry<String,String> constant: constants.entrySet()){
	    String constantName = createUniqueName(constant.getKey());
	    processors.appendChild(createConstantProcessor(constantName, constant.getValue(), doc));

	    // connect the constant to the mobyData creation processor
	    datalinks.appendChild(createDataLinkElement(constantName, "value", 
							processorName, feeds.get(constant.getKey()), 
							doc));
	}

	// feed the id input of the Moby data creator with a spreadsheet importer,
	// so users can easily give a file list of IDs to run.
	String importerName = createUniqueName("SpreadsheetImporter-"+ns.getName()+"-IDs");
	Element importProcessor = doc.createElementNS(T2FLOW_NS, "processor");
	processors.appendChild(importProcessor);
	name = doc.createElementNS(T2FLOW_NS, "name");
	name.appendChild(doc.createTextNode(importerName));
	importProcessor.appendChild(name);
	inputPorts = doc.createElementNS(T2FLOW_NS, "inputPorts");
	importProcessor.appendChild(inputPorts);	
	inputPorts.appendChild(createParamPort("fileurl", "0", doc));

	outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	importProcessor.appendChild(outputPorts);
	String outputUniqueName = createUniqueName(ns.getName());// output name
	outputPorts.appendChild(createParamPort(outputUniqueName, "1", "1", doc));
	
	importProcessor.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));
	activities = doc.createElementNS(T2FLOW_NS, "activities");
	importProcessor.appendChild(activities);
	activity = doc.createElementNS(T2FLOW_NS, "activity");
	activities.appendChild(activity);
	raven = doc.createElementNS(T2FLOW_NS, "raven");
	group = doc.createElementNS(T2FLOW_NS, "group");
	group.appendChild(doc.createTextNode("net.sf.taverna.t2.activities"));
	raven.appendChild(group);
	artifact = doc.createElementNS(T2FLOW_NS, "artifact");
	artifact.appendChild(doc.createTextNode("spreadsheet-import-activity"));
	raven.appendChild(artifact);
	version = doc.createElementNS(T2FLOW_NS, "version");
	version.appendChild(doc.createTextNode("1.0"));
	raven.appendChild(version);
	activity.appendChild(raven);

	clas = doc.createElementNS(T2FLOW_NS, "class");
	activity.appendChild(clas);
	clas.appendChild(doc.createTextNode("net.sf.taverna.t2.activities.spreadsheet.SpreadsheetImportActivity"));	

	    
	inputMap = doc.createElementNS(T2FLOW_NS, "inputMap");
	activity.appendChild(inputMap);
	inputMap.appendChild(createMapFromTo("fileurl", "fileurl", doc)); //identity map

	outputMap = doc.createElementNS(T2FLOW_NS, "outputMap");
	activity.appendChild(outputMap);
	outputMap.appendChild(createMapFromTo(outputUniqueName, outputUniqueName, doc));

	configBean = doc.createElementNS(T2FLOW_NS, "configBean");
	configBean.setAttribute("encoding", "xstream");
	activity.appendChild(configBean);
	Element SpreadsheetImportConfiguration = 
	    doc.createElementNS("", 
				"net.sf.taverna.t2.activities.spreadsheet.SpreadsheetImportConfiguration");
	configBean.appendChild(SpreadsheetImportConfiguration);

	// Assume first column is what we want from the spreadsheet
	Element columnRange = doc.createElementNS("", "columnRange");
	SpreadsheetImportConfiguration.appendChild(columnRange);
	columnRange.appendChild(createElWithText(doc,"","start","0"));
	columnRange.appendChild(createElWithText(doc,"","end","0"));
	columnRange.appendChild(doc.createElementNS("", "excludes"));

	Element rowRange = doc.createElementNS("", "rowRange");
	SpreadsheetImportConfiguration.appendChild(rowRange);
	rowRange.appendChild(createElWithText(doc,"","start","0"));
	rowRange.appendChild(createElWithText(doc,"","end","-1"));
	rowRange.appendChild(doc.createElementNS("", "excludes"));

	SpreadsheetImportConfiguration.appendChild(doc.createElementNS("", "emptyCellValue"));
	
	Element columnNames = doc.createElementNS("", "columnNames");
	SpreadsheetImportConfiguration.appendChild(columnNames);
	Element entry = doc.createElementNS("", "entry");
	columnNames.appendChild(entry);
	entry.appendChild(createElWithText(doc,"","string","A"));
	entry.appendChild(createElWithText(doc,"","string",outputUniqueName));

	SpreadsheetImportConfiguration.appendChild(createElWithText(doc,"","allRows","true"));
	SpreadsheetImportConfiguration.appendChild(createElWithText(doc,"","excludeFirstRow","false"));
	SpreadsheetImportConfiguration.appendChild(createElWithText(doc,"","ignoreBlankRows","true"));
	SpreadsheetImportConfiguration.appendChild(createElWithText(doc,"","emptyCellPolicy","EMPTY_STRING"));
	SpreadsheetImportConfiguration.appendChild(createElWithText(doc,"","outputFormat","PORT_PER_COLUMN"));
	SpreadsheetImportConfiguration.appendChild(createElWithText(doc,"","csvDelimiter",","));

	activity.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));

	importProcessor.appendChild(getDispatchStack(doc));

	iterationStrategyStack = doc.createElementNS(T2FLOW_NS, "iterationStrategyStack");
	importProcessor.appendChild(iterationStrategyStack);
	iteration = doc.createElementNS(T2FLOW_NS, "iteration");
	iterationStrategyStack.appendChild(iteration);
	strategy = doc.createElementNS(T2FLOW_NS, "strategy");
	iteration.appendChild(strategy);
	cross = doc.createElementNS(T2FLOW_NS, "cross");
	strategy.appendChild(cross);
	Element port = doc.createElementNS(T2FLOW_NS, "port");
	cross.appendChild(port);
	port.setAttribute("name", "fileurl");
	port.setAttribute("depth", "0");

	String uniqueInputName = createUniqueName(getInputName(sampleData));
	// todo: sample data may be of more specific type than service requires
	dataFlowInputPorts.appendChild(createWorkflowInputElement(uniqueInputName, sampleData, doc));
	datalinks.appendChild(createWorkflowInputLinkElement(uniqueInputName, 
							     importerName, 
							     "fileurl", 
							     sampleData, 
							     doc));

	// link the spreadsheet reader to the Moby data creator 
	datalinks.appendChild(createDataLinkElement(importerName, outputUniqueName,
						    processorName, "id",
						    doc));

	// return the name of the spreadsheet reading processor and output port so it can be hooked up
	// to input ports for Moby services
	return new String[]{processorName, portNames[portNames.length-1]};
    }

    private Element createConstantProcessor(String constantName, String constantValue, Document doc)
	throws Exception{
	Element processor = doc.createElementNS(T2FLOW_NS, "processor");
	Element name = doc.createElementNS(T2FLOW_NS, "name");
	name.appendChild(doc.createTextNode(constantName));
	processor.appendChild(name);
	
	processor.appendChild(doc.createElementNS(T2FLOW_NS, "inputPorts"));
	Element outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	processor.appendChild(outputPorts); 
	outputPorts.appendChild(createParamPort("value", "0", "0", doc));
	
	processor.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));  
	Element activities = doc.createElementNS(T2FLOW_NS, "activities");
	processor.appendChild(activities);
	Element activity = doc.createElementNS(T2FLOW_NS, "activity");
	activities.appendChild(activity);
	Element raven = doc.createElementNS(T2FLOW_NS, "raven");
	Element group = doc.createElementNS(T2FLOW_NS, "group");
	group.appendChild(doc.createTextNode("net.sf.taverna.t2.activities"));
	raven.appendChild(group);
	Element artifact = doc.createElementNS(T2FLOW_NS, "artifact");
	artifact.appendChild(doc.createTextNode("stringconstant-activity"));
	raven.appendChild(artifact);
	Element version = doc.createElementNS(T2FLOW_NS, "version");
	version.appendChild(doc.createTextNode("1.0"));
	raven.appendChild(version);
	activity.appendChild(raven);
	
	Element clas = doc.createElementNS(T2FLOW_NS, "class");
	activity.appendChild(clas);
	clas.appendChild(doc.createTextNode("net.sf.taverna.t2.activities.stringconstant.StringConstantActivity"));
	
	activity.appendChild(doc.createElementNS(T2FLOW_NS, "inputMap"));
	Element outputMap = doc.createElementNS(T2FLOW_NS, "outputMap");
	activity.appendChild(outputMap);
	outputMap.appendChild(createMapFromTo("value", "value", doc)); //identity map
	
	Element configBean = doc.createElementNS(T2FLOW_NS, "configBean");
	configBean.setAttribute("encoding", "xstream");
	activity.appendChild(configBean);
	Element StringConstantConfigurationBean = 
	    doc.createElementNS("", 
				"net.sf.taverna.t2.activities.stringconstant.StringConstantConfigurationBean");
	configBean.appendChild(StringConstantConfigurationBean);
	Element value = doc.createElementNS("", "value");
	StringConstantConfigurationBean.appendChild(value);
	//all that setup just to get the constant value into the workflow!
	value.appendChild(doc.createTextNode(constantValue));  

	activity.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));
	
	processor.appendChild(getDispatchStack(doc));
	
	Element iterationStrategyStack = doc.createElementNS(T2FLOW_NS, "iterationStrategyStack");
	processor.appendChild(iterationStrategyStack);
	Element iteration = doc.createElementNS(T2FLOW_NS, "iteration");
	iterationStrategyStack.appendChild(iteration);
	Element strategy = doc.createElementNS(T2FLOW_NS, "strategy");
	iteration.appendChild(strategy);
	strategy.appendChild(doc.createElementNS(T2FLOW_NS, "cross"));

	return processor;
    }
    
    private Element createElWithText(Document doc, String elementNS, 
				     String elementName, String textContent){
	Element e = doc.createElementNS(elementNS, elementName);
	e.appendChild(doc.createTextNode(textContent));
	return e;
    }

    // Retrieves the port name that can be used in a datalink.  Since it is of local 
    // scope, we can just call it the same as the moby param.  DataFlowRecorder sets up provenance 
    // URLs (during calls to saveInput) of the form URLOfMobyXML#/path/based/on/Simple[@articleName = "foo"]/datatype
    // We also pass in the processors and datalinks because we may need to add some ourselves
    // When extra processors are added to filter a collection by namespace, etc.
    //
    // If postProcess is true, extra processors may be added to do data decomposition, etc. as spec'ed by the XPath
    //
    // "srcPort" is usually null, because we determine it.  Set it to something specific if a processor doesn't follow
    // the moby service port naming convention (like the regex processor), but you want to add decomposers, etc. as necessary.
    private String[] getPortFromURLRef(URL provenanceURL, MobyPrimaryData sampleData,
				       String srcProcessor, String srcPort,
				       Element processors, Element datalinks,
				       Document doc, boolean postProcess) throws Exception{
	String[] outputProcessorAndPort = new String[2];
	outputProcessorAndPort[0] = srcProcessor; // by default we pass data directly from the source processor
	
	String xpath = provenanceURL.getRef();
	// The string should have the form /path/to/complexType 
	// or /path/based/on[@articleName = "foo"] or /path/based/on[@namespace = "bar"]
	String articleName = null;
	String memberName = null;
	String namespace = null;  // populated if there is a namespace restriction
	String xrefNs = null; // populate if the thing to extract is from the cross-reference block 
	if(xpath.matches("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.COLLECTION+"\\[@"+MobyTags.ARTICLENAME+"\\s*=\\s*'[^']+?\'\\](?:/[^/]+)?$")){
	    articleName = xpath.replaceFirst("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.COLLECTION+
					     "\\[@"+MobyTags.ARTICLENAME+"\\s*=\\s*'([^']+?)'\\](?:/[^/]+)?$", "$1"); 
	}
	else if(xpath.matches("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.SIMPLE+"\\[@"+MobyTags.ARTICLENAME+"\\s*=\\s*'[^']+?'\\]/[^/]+$")){
	    articleName = xpath.replaceFirst("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.SIMPLE+
					     "\\[@"+MobyTags.ARTICLENAME+"\\s*=\\s*'([^']+?)'\\]/[^/]+$", "$1");
	}
	else if(!postProcess){  //use regex that ignore the decomp, etc, xpath parts...we really just want the port name!
	    articleName = xpath.replaceFirst("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.SIMPLE+
					     "\\[@"+MobyTags.ARTICLENAME+"\\s*=\\s*'(.+?)'\\].*", "$1");	    
	    if(articleName.equals(xpath)){  //didn't match a simple
		articleName = xpath.replaceFirst("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.COLLECTION+
						 "\\[@"+MobyTags.ARTICLENAME+"\\s*=\\s*'(.+?)'\\].*", "$1");
	    }
	}
	// Handle the case where the output from one service is not directly used
	// as input to another, but rather a splitter needs to be added to parse out 
	// the relevant information.
	else{
	    /*
	     * Might look like /MOBY/mobyContent/mobyData/Collection[@articleName = 
	     * 'collected_xrefs']/Simple[@articleName = '']/*[@namespace = 'taxon']
	     * if getting Collection members by namespace
	     */
	    if(xpath.matches("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.COLLECTION+"\\[@"+
			     MobyTags.ARTICLENAME+"\\s*=\\s*'.+?\']/"+
			     MobyTags.SIMPLE+"\\[@"+
			     MobyTags.ARTICLENAME+"\\s*=\\s*'.*?'\\]/\\*\\[@"+
			     MobyTags.OBJ_NAMESPACE+"\\s*=\\s*'.+?'\\].*$")){
		String[] spec = xpath.replaceFirst("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.COLLECTION+"\\[@"+
						 MobyTags.ARTICLENAME+"\\s*=\\s*'(.+?)\']/"+
						 MobyTags.SIMPLE+"\\[@"+
						 MobyTags.ARTICLENAME+"\\s*=\\s*'.*?'\\]/\\*\\[@"+
						 MobyTags.OBJ_NAMESPACE+"\\s*=\\s*'(.+?)'\\].*$", "$1\n$2").split("\n");
		// We need to add extra processors to the workflow to filter to just those
		// elements of the collection that are in the correct namespace, so the output 
		// processor changes for the return value
		articleName = spec[0];
		namespace = spec[1];
	    }
	    /**
	     * Might look like /MOBY/mobyContent/mobyData/Simple[@articleName = 'param']/DataType/*[@articleName='member']
	     * which means that we need to grab one of the object members by name from the whole object
	     */
	    else if(xpath.matches("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.SIMPLE+"\\[@"+MobyTags.ARTICLENAME+
				  "\\s*=\\s*'.+?'\\]/.*?/\\*\\[@"+MobyTags.ARTICLENAME+"\\s*=\\s*'.+?'\\]$")){
		String[] spec = xpath.replaceFirst("^.*?"+MobyTags.MOBYDATA+"/"+MobyTags.SIMPLE+
						   "\\[@"+MobyTags.ARTICLENAME+
						   "\\s*=\\s*'(.+?)'\\]/.*?/\\*\\[@"+
						   MobyTags.ARTICLENAME+"\\s*=\\s*'(.+?)'\\]$", "$1\n$2").split("\n");
		articleName = spec[0];
		memberName = spec[1];
	    }
	    /**
	     * Might be a service called on a cross reference for the object, in which case the XPath looks something like
	     * /MOBY/mobyContent/mobyData/Simple[@articleName = 'file']/AminoAcidSequence/CrossReference/*[@namespace = 'taxon']"
	     * as these are db xrefs, they will always be selected by namespace.
	     */
	    else if(xpath.matches("^.*?"+MobyTags.MOBYDATA+"/"+
				  MobyTags.SIMPLE+"\\[@"+MobyTags.ARTICLENAME+
				  "\\s*=\\s*'.+?'\\]/.*?/"+
				  MobyTags.CROSSREFERENCE+"/\\*\\[@"+
				  MobyTags.OBJ_NAMESPACE+"\\s*=\\s*'.+?'\\]")){
		String[] spec = xpath.replaceFirst("^.*?"+MobyTags.MOBYDATA+"/"+
						   MobyTags.SIMPLE+"\\[@"+
						   MobyTags.ARTICLENAME+"\\s*=\\s*'(.+?)\'\\]/.*?/"+
						   MobyTags.CROSSREFERENCE+"/\\*\\[@"+
						   MobyTags.OBJ_NAMESPACE+"\\s*=\\s*'(.+?)'\\]", "$1\n$2").split("\n");
		articleName = spec[0];
		xrefNs = spec[1];
	    }
	    else{
		outputProcessorAndPort[1] = "unimplemented "+xpath;
		return outputProcessorAndPort;
	    }
	}

	// Prepend datatype to get taverna-plugin-required port name format of dataType(articleName)
	// get the data type from the service, not the end of the xpath above, because the type may 
	// be more generic than the specific data in the example.
	MobyService srcService = DataUtils.getService(provenanceURL);
	for(MobyPrimaryData srcOutParam: srcService.getPrimaryOutputs()){
	    if(srcPort != null || srcOutParam.getName().equals(articleName)){
		// srcPort != null means manually overridden, 
		// otherwise get the port, which depends on whether we need 
		// a collection input or not in the next service (sampleData)
		String portName = srcPort == null ? getPortName(srcOutParam, sampleData instanceof MobyPrimaryDataSet) : srcPort; 
		// todo: check that the datatype required by this service is the same of a 
		// supertype of the output from the other service!		
		if(namespace != null){
		    MobyNamespace nsObj = MobyNamespace.getNamespace(namespace, getRegistryFromService(srcService));
		    //		    System.err.println("About to create namespace filter for "+ srcProcessor + " port " + portName + " when srcPort was " + srcPort);
		    return createNamespaceFilter(nsObj, srcProcessor, portName, processors, datalinks, doc);
		}
		else if(xrefNs != null){
		    MobyNamespace nsObj = MobyNamespace.getNamespace(xrefNs, getRegistryFromService(srcService));
		    return createXrefParser(nsObj, srcProcessor, portName, processors, datalinks, doc);
		}
		else if(memberName != null){
		    return createMemberParser(memberName, getRegistryFromService(srcService),
					      srcProcessor, portName, processors, datalinks, doc);
		}
		// Pass data through as-is
		else{
		    outputProcessorAndPort[1] = portName;
		    return outputProcessorAndPort;
		}
	    }
	}
	throw new Exception("Could not find the datatype of output parameter '"+articleName+
			    "' from service '"+srcService.getName()+"'");
    }

    
    /*  Adds a Moby object parser to the workflow that extracts the given object member
       ____________
       srcProcessor
           | ObjectType(paramName)
	   V
       _______________    
       Parse_moby_data    
           | paramName_'memberName' (with ns and id links too)
           V      
       _______________
       Create_moby_data
           | memberType(paramName)
           V

     * Returns output processor/port of Create_moby_data, for linking to downstream processors.
     */
    private String[] createMemberParser(String memberName, Registry serviceRegistry,
					String srcProcessor, String srcPort, 
					Element processors, Element datalinks, Document doc)
	throws Exception{
	String constantName = createUniqueName("Subset_member_"+memberName);
	String memberXPath = "//*[@"+MobyTags.ARTICLENAME+"='"+ memberName + "']";

	return createXPathFilter(constantName, memberXPath, srcProcessor, srcPort, 
				 processors, datalinks, doc);
    }

    // Make the continued use of data X in the workflow conditional on the results of running X through service f
    // and passing the filter condition set on f.  Equivalent to if(f(X) matches f_filter){...}
    // returns [proc name, input port, output port]
    private String[] createServiceConditionFilter(URL conditionURL, String filterRegex, XPathOption filterXPath,
						  boolean caseSensitive, boolean inverse, Document doc,
						  Element inputPorts, Element processors, Element datalinks)
	throws Exception{

	// Open the conditionURL, and find out what service result is used, and what filter applied
 	Document conditionDoc = docBuilder.parse(conditionURL.openStream());
 	MobyService service = DataUtils.getService(conditionDoc);
 	Document condServiceOutputDoc = DataUtils.getInputDoc(conditionDoc);

	// The bits to run the service "f"

 	String dataSrcURLString = conditionURL.toString().replaceFirst("#.*$", ""); //get rid of ref part
 	String processorName = null;
 	// See if the service creating the input for the conditional service has already been added to the workflow
 	if(url2Processor.containsKey(dataSrcURLString)){
 	    processorName = url2Processor.get(dataSrcURLString);
 	}
 	else{
	    processorName = addWorkflowElements(conditionURL, doc, inputPorts, processors, datalinks);
 	}

 	String filterKey = dataSrcURLString+"\n"+filterRegex+"\n"+filterXPath.getXPath()+"\n"+caseSensitive;
 	String[] regexFilterProcNameAndPorts = null;
 	//todo: gimpy loop below, as more than one service output would cause a trampling of regex output ports
 	for(MobyPrimaryData outputParam: service.getPrimaryOutputs()){
 	    // See if the filter on the conditional service has already been used in the workflow
 	    if(filter2Processor.containsKey(filterKey)){
 		regexFilterProcNameAndPorts = filter2Processor.get(filterKey);
 	    }
 	    else{
 		regexFilterProcNameAndPorts = createRegexFilter(filterRegex, 
 							       filterXPath,
							       caseSensitive, 
 							       processorName,
 							       getPortName(outputParam, true),
							       0, //depth of list desired (will return match count)
 							       processors, datalinks, doc);
 		filter2Processor.put(filterKey, regexFilterProcNameAndPorts);
 	    }
 	}

	// We need to flatten the 2-deep list generated by the regex filter's cross product
	String[] beanShellFlattenerProcNameAndPorts = addListFlattenBeanShell(processors, doc);

	int regexOutPort = regexFilterProcNameAndPorts.length-(inverse ? 2 : 1);
	datalinks.appendChild(createDataLinkElement(regexFilterProcNameAndPorts[0],
						    regexFilterProcNameAndPorts[regexOutPort],
						    beanShellFlattenerProcNameAndPorts[0], 
						    beanShellFlattenerProcNameAndPorts[1],
						    doc));

	// Essentially an "if" condition, error if incoming value is empty, missing, or zero
	String[] beanShellFilterProcNameAndPorts = addPassFilterBeanShell(processors, doc);

	datalinks.appendChild(createDataLinkElement(beanShellFlattenerProcNameAndPorts[0],
						    beanShellFlattenerProcNameAndPorts[2],
						    beanShellFilterProcNameAndPorts[0], 
						    beanShellFilterProcNameAndPorts[2],
						    doc));

	return new String[]{beanShellFilterProcNameAndPorts[0], 
                            beanShellFilterProcNameAndPorts[1], 
                            beanShellFilterProcNameAndPorts[3]};
    }

    private String[] createRegexFilter(String regex, XPathOption xpath, boolean caseSensitive, 
				       String srcProcessor, String srcPort, int listDepth, 
 				       Element processors, Element datalinks, Document doc)
 	throws Exception{
	String constantXPathName = createUniqueName(xpath.toString());
	processors.appendChild(createConstantProcessor(constantXPathName, xpath.getXPath(), doc));
	String constantRegexName = createUniqueName(regex);
	processors.appendChild(createConstantProcessor(constantRegexName, regex, doc));
	String constantCaseSensitivityName = createUniqueName("cs_"+caseSensitive);
	processors.appendChild(createConstantProcessor(constantCaseSensitivityName, ""+caseSensitive, doc));

	String[] beanShellFilterProcNameAndPorts = addRegexFilterBeanShell(listDepth, processors, doc);
	
	datalinks.appendChild(createDataLinkElement(srcProcessor,
						    srcPort,
						    beanShellFilterProcNameAndPorts[0], 
						    beanShellFilterProcNameAndPorts[1],
						    doc));	
	datalinks.appendChild(createDataLinkElement(constantRegexName, "value",
						    beanShellFilterProcNameAndPorts[0], 
						    beanShellFilterProcNameAndPorts[2],
						    doc)); 
	datalinks.appendChild(createDataLinkElement(constantCaseSensitivityName, "value",
						    beanShellFilterProcNameAndPorts[0], 
						    beanShellFilterProcNameAndPorts[3],
						    doc)); 
	datalinks.appendChild(createDataLinkElement(constantXPathName, "value",
						    beanShellFilterProcNameAndPorts[0], 
						    beanShellFilterProcNameAndPorts[4],
						    doc)); 
	return new String[]{beanShellFilterProcNameAndPorts[0], 
			    beanShellFilterProcNameAndPorts[5], // false (non-matching) output
			    beanShellFilterProcNameAndPorts[6]}; // true (matching) output
    }
    
    private String[] createXrefParser(MobyNamespace nsObj, String srcProcessor, String srcPort, 
				      Element processors, Element datalinks, Document doc)
	throws Exception{

	String constantName = createUniqueName("Subset_"+MobyTags.CROSSREFERENCE+"_"+nsObj.getName());
	String xrefXPath = "//*[local-name()='"+MobyTags.CROSSREFERENCE+"']/*[@"+
	    MobyTags.OBJ_NAMESPACE+"='"+ nsObj.getName() + "']";

	return createXPathFilter(constantName, xrefXPath, srcProcessor, srcPort, 
				 processors, datalinks, doc);
    }

    /** Adds a beanshell to the workflow that spits out every given xpath match as a moby xml string
	The constantName should be unique in the workflow. */
    private String[] createXPathFilter(String constantName, String xpath, String srcProcessor, String srcPort, 
				       Element processors, Element datalinks, Document doc)
	throws Exception{

	String decompKey = srcProcessor+"\n"+srcPort+"\n"+xpath;
	//	System.err.println("Decomp key is " + decompKey);
	// Has this decomp already been created in another branch?
	if(decomp2Processor.containsKey(decompKey)){
	    return decomp2Processor.get(decompKey);
	}
	
	processors.appendChild(createConstantProcessor(constantName, xpath, doc));
	
	String[] beanShellFilterProcNameAndPorts = addXPathFilterBeanShell(processors, doc);
	
	datalinks.appendChild(createDataLinkElement(srcProcessor,
						    srcPort,
						    beanShellFilterProcNameAndPorts[0], 
						    beanShellFilterProcNameAndPorts[1],
						    doc));	
	datalinks.appendChild(createDataLinkElement(constantName, "value",
						    beanShellFilterProcNameAndPorts[0], 
						    beanShellFilterProcNameAndPorts[2],
						    doc)); 
	String[] procSpec = new String[]{beanShellFilterProcNameAndPorts[0], beanShellFilterProcNameAndPorts[3]};
	decomp2Processor.put(decompKey, procSpec); //in case we need to reuse it in another branch
	return procSpec;
    }

    /* Returns output processor/port of Create_moby_data, for linking to downstream processors */
    private String[] createNamespaceFilter(MobyNamespace nsObj, String srcProcessor, String srcPort, 
					   Element processors, Element datalinks, Document doc)
	throws Exception{
	
	String constantName = createUniqueName("Subset_"+MobyTags.OBJ_NAMESPACE+"_"+nsObj.getName());
	String nsXPath = "//*[@"+MobyTags.OBJ_NAMESPACE+"='"+ nsObj.getName() + "']";
	
	return createXPathFilter(constantName, nsXPath, srcProcessor, srcPort,
				 processors, datalinks, doc);
    }

    // Returns the names of the Moby parsing processor and ports, so we can create the data links in the caller
    // 1 input port, two or more output ports depending on the type
    private String[] addMobyDataParserProcessor(MobyPrimaryData mobyData, Element processors, Document doc) 
	throws Exception{
	String dataType = null;
	MobyDataType mobyDataType = null;
	if(mobyData instanceof MobyPrimaryDataSet){
	    dataType = "Collections - unimplemented";
	}
	else if(mobyData instanceof MobyPrimaryDataSimple){
	    if(mobyData instanceof MobyDataComposite){
		mobyDataType = mobyData.getDataType();
		dataType = mobyDataType.getName();		
	    }
	    // a Moby primitive
	    else if(mobyData instanceof MobyDataBoolean ||
		    mobyData instanceof MobyDataBytes ||
		    mobyData instanceof MobyDataDateTime ||
		    mobyData instanceof MobyDataFloat ||
		    mobyData instanceof MobyDataInt ||
		    mobyData instanceof MobyDataString){
		dataType = "Primitive datatype - unimplemented";
	    }
	    // base object or xref
	    else{
		dataType = "Object";
	    }
	}
	String parserProcName = createUniqueName("Parse Moby Data("+dataType+")");

	Element parserProcessor = doc.createElementNS(T2FLOW_NS, "processor");
	processors.appendChild(parserProcessor);
	parserProcessor.appendChild(createElWithText(doc, T2FLOW_NS, "name", parserProcName));

	Element inputPorts = doc.createElementNS(T2FLOW_NS, "inputPorts");
	parserProcessor.appendChild(inputPorts);
	String inputName = "mobyData('"+dataType+"')";
	inputPorts.appendChild(createParamPort(inputName, "0", doc));

	Element outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	parserProcessor.appendChild(outputPorts);
	outputPorts.appendChild(createParamPort("id", "1", "1", doc));
	outputPorts.appendChild(createParamPort("namespace", "1", "1", doc));

	parserProcessor.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));

	Element activities = doc.createElementNS(T2FLOW_NS, "activities");
	parserProcessor.appendChild(activities);
	Element activity = doc.createElementNS(T2FLOW_NS, "activity");
	activities.appendChild(activity);
	Element raven = doc.createElementNS(T2FLOW_NS, "raven");
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "group", "net.sf.taverna.t2.activities"));
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "artifact", "biomoby-activity"));
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "version", "1.0"));
	activity.appendChild(raven);

	activity.appendChild(createElWithText(doc, T2FLOW_NS, "class", 
					      "net.sf.taverna.t2.activities.biomoby.MobyParseDatatypeActivity"));

	Element inputMap = doc.createElementNS(T2FLOW_NS, "inputMap");
	activity.appendChild(inputMap);
	inputMap.appendChild(createMapFromTo(inputName, inputName, doc));
	Element outputMap = doc.createElementNS(T2FLOW_NS, "outputMap");
	activity.appendChild(outputMap);
	outputMap.appendChild(createMapFromTo("id", "id", doc));
	outputMap.appendChild(createMapFromTo("namespace", "namespace", doc));
	if(mobyDataType != null){
	    for(MobyRelationship child: mobyDataType.getAllChildren()){
		String childName = getParserOutputName(mobyData.getName(), child.getName());
		outputMap.appendChild(createMapFromTo(childName, childName, doc));
		outputMap.appendChild(createMapFromTo(childName+"_ns", childName+"_ns", doc));
		outputMap.appendChild(createMapFromTo(childName+"_id", childName+"_id", doc));
		outputPorts.appendChild(createParamPort(childName, "1", "1", doc));
		outputPorts.appendChild(createParamPort(childName+"_ns", "1", "1", doc));
		outputPorts.appendChild(createParamPort(childName+"_id", "1", "1", doc));
	    }
	}

	Element configBean = doc.createElementNS(T2FLOW_NS, "configBean");
	configBean.setAttribute("encoding", "xstream");
	activity.appendChild(configBean);
	Element ParseActivityConfigurationBean = 
	    doc.createElementNS("", 
				"net.sf.taverna.t2.activities.biomoby.MobyParseDatatypeActivityConfigurationBean");
	configBean.appendChild(ParseActivityConfigurationBean);
	ParseActivityConfigurationBean.appendChild(createElWithText(doc, "", "datatypeName", 
								    mobyData.getDataType().getName()));
	ParseActivityConfigurationBean.appendChild(createElWithText(doc, "", "registryEndpoint", 
								    getCentralEndpointFromMobyData(mobyData)));
	ParseActivityConfigurationBean.appendChild(createElWithText(doc, "", "articleNameUsedByService", 
								    mobyData.getName()));
	
	activity.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));

	parserProcessor.appendChild(getDispatchStack(doc));

	Element iterationStrategyStack = doc.createElementNS(T2FLOW_NS, "iterationStrategyStack");
	parserProcessor.appendChild(iterationStrategyStack);
	Element iteration = doc.createElementNS(T2FLOW_NS, "iteration");
	iterationStrategyStack.appendChild(iteration);
	Element strategy = doc.createElementNS(T2FLOW_NS, "strategy");
	iteration.appendChild(strategy);
	Element cross = doc.createElementNS(T2FLOW_NS, "cross");
	strategy.appendChild(cross);
	
	Element port = doc.createElementNS(T2FLOW_NS, "port");
	cross.appendChild(port);
	port.setAttribute("depth", "0");
	port.setAttribute("name", inputName);

	return new String[]{parserProcName, inputName, "id", "namespace"};//todo: add member names
    }

    // Follows the naming convention for the moby data parser on complex objects
    private String getParserOutputName(String objectParamName, String childMemberName){
	return objectParamName+"_'"+childMemberName+"'";
    }

    private String getCentralEndpointFromMobyData(MobyPrimaryData mobyData){
	String endpoint = null;
	if(mobyData instanceof MobyDataObject && ((MobyDataObject) mobyData).getPrimaryNamespace() != null &&
	   ((MobyDataObject) mobyData).getPrimaryNamespace().getRegistry() != null){
	    endpoint = ((MobyDataObject) mobyData).getPrimaryNamespace().getRegistry().getEndpoint();
	}
	else if(mobyData.getDataType().getRegistry() != null){
	    endpoint = mobyData.getDataType().getRegistry().getEndpoint();
	}
	else{
	    endpoint = CentralImpl.getDefaultURL();
	}
	return endpoint;
    }

    // Returns the name of the moby object building processor and its ports, so we can create the data links in the caller
    private String[] addMobyDataCreatorProcessor(MobyPrimaryData mobyData, Element processors, Document doc) 
	throws Exception{
	String dataType = null;
	boolean primitiveDataType = false;
	if(mobyData instanceof MobyPrimaryDataSet){
	    dataType = "Collections - unimplemented";
	}
	else if(mobyData instanceof MobyPrimaryDataSimple){
	    if(mobyData instanceof MobyDataComposite){
		dataType = "Composite datatype - unimplemented";
	    }
	    // a Moby primitive
	    else if(mobyData instanceof MobyDataString){
		dataType = MobyTags.MOBYSTRING;
		primitiveDataType = true;
	    }
	    else if(mobyData instanceof MobyDataBoolean){
		dataType = MobyTags.MOBYBOOLEAN;
		primitiveDataType = true;
	    }
	    else if(mobyData instanceof MobyDataDateTime){
		dataType = MobyTags.MOBYDATETIME;
		primitiveDataType = true;
	    }
	    else if(mobyData instanceof MobyDataFloat){
		dataType = MobyTags.MOBYFLOAT;
		primitiveDataType = true;
	    }
	    else if(mobyData instanceof MobyDataInt){
		dataType = MobyTags.MOBYINTEGER;
		primitiveDataType = true;
	    }
	    // base object or xref
	    else{
		MobyNamespace[] nss = ((MobyPrimaryDataSimple) mobyData).getNamespaces();
		if(nss.length != 0){
		    dataType = nss[0].getName()+"_ID";
		}
		else{
		    dataType = "Object";
		}
	    }
	}

	String builderProcName = createUniqueName("Create_"+dataType+"s");
	Element builderProcessor = doc.createElementNS(T2FLOW_NS, "processor");
	processors.appendChild(builderProcessor);
	builderProcessor.appendChild(createElWithText(doc, T2FLOW_NS, "name", builderProcName));

	Element inputPorts = doc.createElementNS(T2FLOW_NS, "inputPorts");
	builderProcessor.appendChild(inputPorts);
	inputPorts.appendChild(createParamPort("id", "0", doc));
	inputPorts.appendChild(createParamPort("namespace", "0", doc));
	if(primitiveDataType){
	    inputPorts.appendChild(createParamPort("value", "0", doc));
	}

	Element outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	builderProcessor.appendChild(outputPorts);
	outputPorts.appendChild(createParamPort("mobyData", "0", "0", doc));

	builderProcessor.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));

	Element activities = doc.createElementNS(T2FLOW_NS, "activities");
	builderProcessor.appendChild(activities);
	Element activity = doc.createElementNS(T2FLOW_NS, "activity");
	activities.appendChild(activity);
	Element raven = doc.createElementNS(T2FLOW_NS, "raven");
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "group", "net.sf.taverna.t2.activities"));
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "artifact", "biomoby-activity"));
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "version", "1.0"));
	activity.appendChild(raven);

	activity.appendChild(createElWithText(doc, T2FLOW_NS, "class", 
					      "net.sf.taverna.t2.activities.biomoby.BiomobyObjectActivity"));

	Element inputMap = doc.createElementNS(T2FLOW_NS, "inputMap");
	activity.appendChild(inputMap);
	inputMap.appendChild(createMapFromTo("id", "id", doc));
	inputMap.appendChild(createMapFromTo("namespace", "namespace", doc));	
	if(primitiveDataType){
	    inputMap.appendChild(createMapFromTo("value", "value", doc));
	}
	Element outputMap = doc.createElementNS(T2FLOW_NS, "outputMap");
	activity.appendChild(outputMap);
	outputMap.appendChild(createMapFromTo("mobyData", "mobyData", doc));
	
	Element configBean = doc.createElementNS(T2FLOW_NS, "configBean");
	configBean.setAttribute("encoding", "xstream");
	activity.appendChild(configBean);
	Element BuildActivityConfigurationBean = 
	    doc.createElementNS("", 
				"net.sf.taverna.t2.activities.biomoby.BiomobyObjectActivityConfigurationBean");
	configBean.appendChild(BuildActivityConfigurationBean);
	BuildActivityConfigurationBean.appendChild(createElWithText(doc, "", "mobyEndpoint", 
								    getCentralEndpointFromMobyData(mobyData)));
	BuildActivityConfigurationBean.appendChild(createElWithText(doc, "", "serviceName", 
								    mobyData.getDataType().getName()));
	BuildActivityConfigurationBean.appendChild(doc.createElementNS("", "authorityName"));
	
	activity.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));

	builderProcessor.appendChild(getDispatchStack(doc));

	Element iterationStrategyStack = doc.createElementNS(T2FLOW_NS, "iterationStrategyStack");
	builderProcessor.appendChild(iterationStrategyStack);
	Element iteration = doc.createElementNS(T2FLOW_NS, "iteration");
	iterationStrategyStack.appendChild(iteration);
	Element strategy = doc.createElementNS(T2FLOW_NS, "strategy");
	iteration.appendChild(strategy);
	Element dot = doc.createElementNS(T2FLOW_NS, "dot");
	strategy.appendChild(dot);

	Element port = doc.createElementNS(T2FLOW_NS, "port");
	dot.appendChild(port);
	port.setAttribute("depth", "0");
	port.setAttribute("name", "id");
	port = doc.createElementNS(T2FLOW_NS, "port");
	dot.appendChild(port);
	port.setAttribute("depth", "0");
	port.setAttribute("name", "namespace");
	if(primitiveDataType){
	    port = doc.createElementNS(T2FLOW_NS, "port");
	    dot.appendChild(port);
	    port.setAttribute("depth", "0");
	    port.setAttribute("name", "value");
	    return new String[]{builderProcName, "id", "namespace", "value", "mobyData"};
	}
	else{
	    return new String[]{builderProcName, "id", "namespace", "mobyData"};
	}
    }

   private String[] addListFlattenBeanShell(Element processors, Document doc) 
	throws Exception{
	String beanShellProcName = createUniqueName("Create_Pass_Fail_List");
	
	// Now do all the param-specific stuff below (inserts into various parts of the elements defined above)
	Map<String,String> inputsMap = new LinkedHashMap<String,String>();  
	// linked because order is important to line up port connections
	Map<String,String> inputTypes = new LinkedHashMap<String,String>();
	inputsMap.put("inputlist", "2");
	inputTypes.put("inputlist", "text/plain");
	Map<String,String> outputsMap = new LinkedHashMap<String,String>();
	outputsMap.put("outputlist", "1");

	return addBeanShell(beanShellProcName, "dot",
			    inputsMap, inputTypes, outputsMap, 
			    getListFlattenScript(), new String[]{}, 
			    processors, doc);	
    }    

    private String[] addPassFilterBeanShell(Element processors, Document doc) 
	throws Exception{
	String beanShellProcName = createUniqueName("IfPassesContentFilter");
	
	// Now do all the param-specific stuff below (inserts into various parts of the elements defined above)
	Map<String,String> inputsMap = new LinkedHashMap<String,String>();  
	// linked because order is important to line up port connections
	Map<String,String> inputTypes = new LinkedHashMap<String,String>();
	inputsMap.put("dataToPassThrough", "1");
	inputsMap.put("conditionResults", "1");
	inputTypes.put("dataToPassThrough", "text/xml");
	inputTypes.put("conditionResults", "text/plain");
	Map<String,String> outputsMap = new LinkedHashMap<String,String>();
	outputsMap.put("dataPassed", "1");

	return addBeanShell(beanShellProcName, "cross",
			    inputsMap, inputTypes, outputsMap, 
			    getPassFilterScript(), new String[]{}, 
			    processors, doc);	
    }    

    // Returns the name of the bean shell processor and its ports, so we can create the data links in the caller
    // The input is the original moby xml, and the xpath to extract, and the regex to apply to the text contents of the xpath results.
    // The output is a list of moby xml docs, one for each mobyData that fit the xpath and regex criteria.
    private String[] addRegexFilterBeanShell(int listDepth, Element processors, Document doc) 
	throws Exception{
	String beanShellProcName = createUniqueName("Filter"+(listDepth == 0 ? "_Match_Count" : "_By_Content"));
	
	// Now do all the param-specific stuff below (inserts into various parts of the elements defined above)
	Map<String,String> inputsMap = new LinkedHashMap<String,String>();  
	// linked because order is important to line up port connections
	Map<String,String> inputTypes = new LinkedHashMap<String,String>();
	inputsMap.put("xml_text", "0");
	inputsMap.put("regex", "0");
        inputsMap.put("case_sensitive", "0");
	inputsMap.put("xpath", "0");
	inputTypes.put("xml_text", "text/xml");
	inputTypes.put("xpath", "text/plain");
	inputTypes.put("regex", "text/plain");
	inputTypes.put("case_sensitive", "text/plain");
	Map<String,String> outputsMap = new LinkedHashMap<String,String>();
	if(listDepth == 0){
	    outputsMap.put("FALSE_matchCount", "0");
	    outputsMap.put("TRUE_matchCount", "0");
        }
        else{
            outputsMap.put("FALSE_nodelistAsXML", ""+listDepth);
            outputsMap.put("TRUE_nodelistAsXML", ""+listDepth);
        }

	return addBeanShell(beanShellProcName, "cross",
			    inputsMap, inputTypes, outputsMap, 
			    getRegexFilterScript(listDepth), new String[]{"dom4j:dom4j:1.6"}, 
			    processors, doc);	
    }

    // Returns the name of the bean shell processor and its ports, so we can create the data links in the caller
    // The input is the original moby xml, and the xpath to apply.  The output is a list of moby xml docs, one for each xpath match.
    private String[] addXPathFilterBeanShell(Element processors, Document doc) 
	throws Exception{
	String beanShellProcName = createUniqueName("Extract_Moby_Subset");

	// Now do all the param-specific stuff below (inserts into various parts of the elements defined above)
	Map<String,String> inputsMap = new LinkedHashMap<String,String>();
	// linked because order is importtant to line up port connections
	Map<String,String> inputTypes = new LinkedHashMap<String,String>();
	inputsMap.put("xml_text", "0");
	inputsMap.put("xpath", "0");
	inputTypes.put("xml_text", "text/xml");
	inputTypes.put("xpath", "text/plain");
	Map<String,String> outputsMap = new HashMap<String,String>();
	outputsMap.put("nodelistAsXML", "1");

	return addBeanShell(beanShellProcName, "cross",
			    inputsMap, inputTypes, outputsMap, 
			    getXPathFilterScript(), new String[]{"dom4j:dom4j:1.6"}, 
			    processors, doc);
    }

    private String[] addBeanShell(String beanShellProcName, String vectorComboOp,
				  Map<String,String> inputsMap, Map<String,String> inputTypes, 
				  Map<String,String> outputsMap, String script, String[] dependencySpecs,
				  Element processors, Document doc) throws Exception{
	Element beanShellProcessor = doc.createElementNS(T2FLOW_NS, "processor");
	processors.appendChild(beanShellProcessor);
	beanShellProcessor.appendChild(createElWithText(doc, T2FLOW_NS, "name", beanShellProcName));

	Element inputPorts = doc.createElementNS(T2FLOW_NS, "inputPorts");
	beanShellProcessor.appendChild(inputPorts);
	Element outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	beanShellProcessor.appendChild(outputPorts);
	beanShellProcessor.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));
	
	Element activities = doc.createElementNS(T2FLOW_NS, "activities");
	beanShellProcessor.appendChild(activities);
	Element activity = doc.createElementNS(T2FLOW_NS, "activity");
	activities.appendChild(activity);
	Element raven = doc.createElementNS(T2FLOW_NS, "raven");
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "group", "net.sf.taverna.t2.activities"));
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "artifact", "beanshell-activity"));
	raven.appendChild(createElWithText(doc, T2FLOW_NS, "version", "1.0"));
	activity.appendChild(raven);

	activity.appendChild(createElWithText(doc, T2FLOW_NS, "class", 
					      "net.sf.taverna.t2.activities.beanshell.BeanshellActivity"));

	Element inputMap = doc.createElementNS(T2FLOW_NS, "inputMap");
	activity.appendChild(inputMap);
	Element outputMap = doc.createElementNS(T2FLOW_NS, "outputMap");
	activity.appendChild(outputMap);

	Element configBean = doc.createElementNS(T2FLOW_NS, "configBean");
	configBean.setAttribute("encoding", "xstream");
	activity.appendChild(configBean);
	Element BeanShellActivityConfigurationBean = 
	    doc.createElementNS("", 
				"net.sf.taverna.t2.activities.beanshell.BeanshellActivityConfigurationBean");
	configBean.appendChild(BeanShellActivityConfigurationBean);
	BeanShellActivityConfigurationBean.appendChild(createElWithText(doc, "", "script", script));
			  
	Element dependencies = doc.createElementNS("", "dependencies");
	BeanShellActivityConfigurationBean.appendChild(dependencies);
	for(String dep: dependencySpecs){
	    dependencies.appendChild(createElWithText(doc, "", "string", dep));
	}
	BeanShellActivityConfigurationBean.appendChild(createElWithText(doc, "", 
									"classLoaderSharing", "workflow"));
	BeanShellActivityConfigurationBean.appendChild(doc.createElementNS("", "localDependencies"));
	BeanShellActivityConfigurationBean.appendChild(doc.createElementNS("", "artifactDependencies"));

	Element inputs = doc.createElementNS("", "inputs");
	BeanShellActivityConfigurationBean.appendChild(inputs);
	Element outputs = doc.createElementNS("", "outputs");
	BeanShellActivityConfigurationBean.appendChild(outputs);

	beanShellProcessor.appendChild(getDispatchStack(doc));
	activity.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));	 

	Element iterationStrategyStack = doc.createElementNS(T2FLOW_NS, "iterationStrategyStack");
	beanShellProcessor.appendChild(iterationStrategyStack);
	Element iteration = doc.createElementNS(T2FLOW_NS, "iteration");
	iterationStrategyStack.appendChild(iteration);
	Element strategy = doc.createElementNS(T2FLOW_NS, "strategy");
	iteration.appendChild(strategy);
	Element vectorOp = null;
        if(vectorComboOp != null){
            vectorOp = doc.createElementNS(T2FLOW_NS, vectorComboOp);
	    strategy.appendChild(vectorOp); // either cross or dot
        }

	// Lst processor name, input ports and output ports, in that order.
	Vector<String> returnSpec = new Vector<String>();
	returnSpec.add(beanShellProcName);

	for(Map.Entry<String,String> input: inputsMap.entrySet()){
	    returnSpec.add(input.getKey());
	    inputPorts.appendChild(createParamPort(input.getKey(), input.getValue(), doc));
	    inputMap.appendChild(createMapFromTo(input.getKey(), input.getKey(), doc));

	    Element ActivityInputPortDefinitionBean = doc.createElementNS("", 
	        "net.sf.taverna.t2.workflowmodel.processor.activity.config.ActivityInputPortDefinitionBean");
	    inputs.appendChild(ActivityInputPortDefinitionBean);
	    ActivityInputPortDefinitionBean.appendChild(doc.createElementNS("", "handledReferenceSchemes"));
	    ActivityInputPortDefinitionBean.appendChild(createElWithText(doc, "", "translatedElementType", "java.lang.String"));
	    ActivityInputPortDefinitionBean.appendChild(createElWithText(doc, "", "allowsLiteralValues", "true"));
	    ActivityInputPortDefinitionBean.appendChild(createElWithText(doc, "", "name", input.getKey()));
	    ActivityInputPortDefinitionBean.appendChild(createElWithText(doc, "", "depth", input.getValue()));
	    Element mimeTypes = doc.createElementNS("", "mimeTypes");
	    ActivityInputPortDefinitionBean.appendChild(mimeTypes);
	    mimeTypes.appendChild(createElWithText(doc, "", "string", inputTypes.get(input.getKey())));

	    Element port = doc.createElementNS(T2FLOW_NS, "port");
	    if(vectorOp != null){
                vectorOp.appendChild(port);
            }
	    port.setAttribute("depth", input.getValue());
	    port.setAttribute("name", input.getKey());
	}
	for(Map.Entry<String,String> output: outputsMap.entrySet()){
	    returnSpec.add(output.getKey());
	    outputPorts.appendChild(createParamPort(output.getKey(), output.getValue(), output.getValue(), doc));
	    outputMap.appendChild(createMapFromTo(output.getKey(), output.getKey(), doc));

	    Element ActivityOutputPortDefinitionBean = doc.createElementNS("", 
	        "net.sf.taverna.t2.workflowmodel.processor.activity.config.ActivityOutputPortDefinitionBean");
	    outputs.appendChild(ActivityOutputPortDefinitionBean);
	    ActivityOutputPortDefinitionBean.appendChild(createElWithText(doc, "", "granularDepth", output.getValue()));
	    ActivityOutputPortDefinitionBean.appendChild(createElWithText(doc, "", "name", output.getKey()));
	    ActivityOutputPortDefinitionBean.appendChild(createElWithText(doc, "", "depth", output.getValue()));
	    ActivityOutputPortDefinitionBean.appendChild(doc.createElementNS("", "mimeTypes"));
	}
		
	// names below must match i/o map keys above
	return returnSpec.toArray(new String[returnSpec.size()]);
    }

    private Element createMapFromTo(String from, String to, Document doc){
	Element map = doc.createElementNS(T2FLOW_NS, "map");
	map.setAttribute("from", from);
	map.setAttribute("to", to);
	return map;
    }

    private Element createParamPort(String name, String depth, String granularDepth, Document doc){
	Element port = createParamPort(name, depth, doc);
	Element gD = doc.createElementNS(T2FLOW_NS, "granularDepth");
	gD.appendChild(doc.createTextNode(granularDepth));
	port.appendChild(gD);
	return port;
    }

    private Element createParamPort(String name, String depth, Document doc){
	Element port = doc.createElementNS(T2FLOW_NS, "port");
	Element nameEl = doc.createElementNS(T2FLOW_NS, "name");
	nameEl.appendChild(doc.createTextNode(name));
	port.appendChild(nameEl);
	Element depthEl = doc.createElementNS(T2FLOW_NS, "depth");
	depthEl.appendChild(doc.createTextNode(depth));
	port.appendChild(depthEl);
	return port;
    }



    private Element createWorkflowInputLinkElement(String inputName, String processorName,
						   String processorParamPortName, 
						   MobyPrimaryData sampleData, Document doc){
	Element datalink = doc.createElementNS(T2FLOW_NS, "datalink");

	Element sink = doc.createElementNS(T2FLOW_NS, "sink");
	sink.setAttribute("type", "processor");
	datalink.appendChild(sink);

	Element processorSink = doc.createElementNS(T2FLOW_NS, "processor");
	processorSink.appendChild(doc.createTextNode(processorName));
	sink.appendChild(processorSink);

	Element portSink = doc.createElementNS(T2FLOW_NS, "port");
	portSink.appendChild(doc.createTextNode(processorParamPortName));
	sink.appendChild(portSink);
	
	Element src = doc.createElementNS(T2FLOW_NS, "source");
	src.setAttribute("type", "dataflow");
	datalink.appendChild(src);

        Element port = doc.createElementNS(T2FLOW_NS, "port");
	port.appendChild(doc.createTextNode(inputName));
	src.appendChild(port);

	return datalink;
    }

    private Element createWorkflowOutputLinkElement(String processorName, String processorPortName, 
						    String outputParamPortName, Document doc){
	Element datalink = doc.createElementNS(T2FLOW_NS, "datalink");

	Element sink = doc.createElementNS(T2FLOW_NS, "sink");
	sink.setAttribute("type", "dataflow");
	datalink.appendChild(sink);

	Element port = doc.createElementNS(T2FLOW_NS, "port");
	port.appendChild(doc.createTextNode(outputParamPortName));
	sink.appendChild(port);

	Element src = doc.createElementNS(T2FLOW_NS, "source");
	src.setAttribute("type", "processor");
	datalink.appendChild(src);

	Element processorSrc = doc.createElementNS(T2FLOW_NS, "processor");
	processorSrc.appendChild(doc.createTextNode(processorName));
	src.appendChild(processorSrc);

	Element portSrc = doc.createElementNS(T2FLOW_NS, "port");
	portSrc.appendChild(doc.createTextNode(processorPortName));
	src.appendChild(portSrc);	
	return datalink;
    }

    private Element createDataLinkElement(String serviceProcessorName1, String outParamPortName, 
					  String serviceProcessorName2, String inParamPortName, 
					  Document doc){
	Element datalink = doc.createElementNS(T2FLOW_NS, "datalink");

	Element sink = doc.createElementNS(T2FLOW_NS, "sink");
	sink.setAttribute("type", "processor");
	datalink.appendChild(sink);

	Element processor = doc.createElementNS(T2FLOW_NS, "processor");
	processor.appendChild(doc.createTextNode(serviceProcessorName2));
	sink.appendChild(processor);

	Element port = doc.createElementNS(T2FLOW_NS, "port");
	port.appendChild(doc.createTextNode(inParamPortName));
	sink.appendChild(port);

	Element src = doc.createElementNS(T2FLOW_NS, "source");
	src.setAttribute("type", "processor");
	datalink.appendChild(src);

	Element processorSrc = doc.createElementNS(T2FLOW_NS, "processor");
	processorSrc.appendChild(doc.createTextNode(serviceProcessorName1));
	src.appendChild(processorSrc);

	Element portSrc = doc.createElementNS(T2FLOW_NS, "port");
	portSrc.appendChild(doc.createTextNode(outParamPortName));
	src.appendChild(portSrc);

	return datalink;
    }

    private synchronized Transformer getTransformer() throws Exception{
	if(nullTransformer == null){
	    nullTransformer = TransformerFactory.newInstance().newTransformer();
	}
	return nullTransformer;
    }

    //Ensures a unique name for a workflow element (e.g. same Moby service may be called more than once)
    private String createUniqueName(String preferredName){
	if(namesUsed.containsKey(preferredName)){
	    namesUsed.put(preferredName, namesUsed.get(preferredName).intValue()+1); //increment
	    preferredName += "_"+namesUsed.get(preferredName);
	}
	else{
	    namesUsed.put(preferredName, 1); // will be auto-boxed to Integer
	}
	return preferredName;
    }

    // dataType(articleName) as required by the Taverna Moby plugin
    private String getPortName(MobyPrimaryData data, boolean asCollection){
	if(data instanceof MobyPrimaryDataSet){
	    if(asCollection){
		return data.getDataType().getName()+"(Collection - '"+data.getName()+"')";
	    }
	    else{
		return data.getDataType().getName()+"(Collection - '"+data.getName()+"' As Simples)";
	    }
	}
	else{
	    return data.getDataType().getName()+"("+data.getName()+")";
	}
    }

    // A useful human-legible name for a workflow input
    private String getInputName(MobyPrimaryData data){
	String dataType = data.getDataType().getName();
	if(dataType.equals(MobyTags.MOBYOBJECT)){	    
	    MobyNamespace[] namespaces = data.getNamespaces();
	    if(namespaces.length == 0){
		return "FileOf"+dataType+"-"+data.getName()+"-IDs";
	    }
	    else{
		// Note: we could concatenate all allowed namespaces, but the service wouldn't 
		// necessarily return the same results for all input namespaces, so we'll be
		// safe and stick to the primary one.
		return "FileOf"+namespaces[0].getName()+"-"+data.getName()+"IDs";
	    }
	}
	else{
	    return dataType+"-"+data.getName();
	}
    }

    // Turn a Moby service call into a Taverna processor
    private Element createProcessorElement(MobyService service, MobyDataJob sampleInput, Document doc)
	throws Exception{
	Element processor = doc.createElementNS(T2FLOW_NS, "processor");
	Element name = doc.createElementNS(T2FLOW_NS, "name");
	name.appendChild(doc.createTextNode(createUniqueName(service.getName())));
	processor.appendChild(name);

	Element inputPorts = doc.createElementNS(T2FLOW_NS, "inputPorts");
	processor.appendChild(inputPorts);
	for(MobyPrimaryData inParam: service.getPrimaryInputs()){
	    if(inParam instanceof MobyPrimaryDataSet){
		inputPorts.appendChild(createParamPort(getPortName(inParam, true), "1", "1", doc));
	    }
	    else{
		inputPorts.appendChild(createParamPort(getPortName(inParam, true), "0", doc));
	    }
	}
	
	Element outputPorts = doc.createElementNS(T2FLOW_NS, "outputPorts");
	processor.appendChild(outputPorts);
	for(MobyPrimaryData outParam: service.getPrimaryOutputs()){
	    if(outParam instanceof MobyPrimaryDataSet){
		// create a the collection and As Simple ports as the Moby plug-in for Taverna does
		outputPorts.appendChild(createParamPort(getPortName(outParam, true), "1", "1", doc));
		outputPorts.appendChild(createParamPort(getPortName(outParam, false), "1", "1", doc));
	    }
	    else{
		outputPorts.appendChild(createParamPort(getPortName(outParam, false), "0", "0", doc));		
	    }
	}
	// Every Moby service has a output port called 'output' containing the whole Moby XML payload
	outputPorts.appendChild(createParamPort("output", "0", "0", doc));
	
	processor.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));

	Element activities = doc.createElementNS(T2FLOW_NS, "activities");
	processor.appendChild(activities);
	Element activity = doc.createElementNS(T2FLOW_NS, "activity");
	activities.appendChild(activity);
	Element raven = doc.createElementNS(T2FLOW_NS, "raven");
	Element group = doc.createElementNS(T2FLOW_NS, "group");
	group.appendChild(doc.createTextNode("net.sf.taverna.t2.activities"));
	raven.appendChild(group);
	Element artifact = doc.createElementNS(T2FLOW_NS, "artifact");
	artifact.appendChild(doc.createTextNode("biomoby-activity"));
	raven.appendChild(artifact);
	Element version = doc.createElementNS(T2FLOW_NS, "version");
	version.appendChild(doc.createTextNode("1.0"));
	raven.appendChild(version);
	activity.appendChild(raven);

	Element clas = doc.createElementNS(T2FLOW_NS, "class");
	activity.appendChild(clas);
	clas.appendChild(doc.createTextNode("net.sf.taverna.t2.activities.biomoby.BiomobyActivity"));

	Element inputMap = doc.createElementNS(T2FLOW_NS, "inputMap");
	activity.appendChild(inputMap);
	for(MobyPrimaryData inParam: service.getPrimaryInputs()){
	    // identity map
	    inputMap.appendChild(createMapFromTo(getPortName(inParam, true), getPortName(inParam, true), doc));  
	}
	Element outputMap = doc.createElementNS(T2FLOW_NS, "outputMap");
	activity.appendChild(outputMap);
	for(MobyPrimaryData outParam: service.getPrimaryOutputs()){
	    // identity map
	    if(outParam instanceof MobyPrimaryDataSet){
		// create a the collection and As Simple ports as the Moby plug-in for Taverna does
		outputMap.appendChild(createMapFromTo(getPortName(outParam, true), getPortName(outParam, true), doc));
		outputMap.appendChild(createMapFromTo(getPortName(outParam, false), getPortName(outParam, false), doc));
	    }
	    else{
		outputMap.appendChild(createMapFromTo(getPortName(outParam, false), getPortName(outParam, false), doc));
	    }
	}
	// The standard 'output' port on all Moby services
	outputMap.appendChild(createMapFromTo("output", "output", doc));

	Element configBean = doc.createElementNS(T2FLOW_NS, "configBean");
	configBean.setAttribute("encoding", "xstream");
	activity.appendChild(configBean);
	Element BiomobyActivityConfigurationBean = 
	    doc.createElementNS("", 
				"net.sf.taverna.t2.activities.biomoby.BiomobyActivityConfigurationBean");
	configBean.appendChild(BiomobyActivityConfigurationBean);

	MobyServiceType serviceType = service.getServiceType();
	String central = getRegistryFromService(service).getEndpoint();
	Element mobyEndpoint = doc.createElementNS("", "mobyEndpoint");
	mobyEndpoint.appendChild(doc.createTextNode(central == null ? "" : central));
	BiomobyActivityConfigurationBean.appendChild(mobyEndpoint);

	Element serviceName = doc.createElementNS("", "serviceName");
	serviceName.appendChild(doc.createTextNode(service.getName()));
	BiomobyActivityConfigurationBean.appendChild(serviceName);

	Element authorityName = doc.createElementNS("", "authorityName");
	authorityName.appendChild(doc.createTextNode(service.getAuthority()));

	Element category = doc.createElementNS("", "category");
	category.appendChild(doc.createTextNode(service.getCategory()));
	BiomobyActivityConfigurationBean.appendChild(category);

	Element serviceTypeEl = doc.createElementNS("", "serviceType");
	serviceTypeEl.appendChild(doc.createTextNode(serviceType.getName()));
	BiomobyActivityConfigurationBean.appendChild(serviceTypeEl);

	Element secondaries = doc.createElementNS("", "secondaries");
	BiomobyActivityConfigurationBean.appendChild(secondaries);
	for(MobySecondaryData secParam: service.getSecondaryInputs()){
	    Element entry = doc.createElementNS("", "entry");

	    Element stringName = doc.createElementNS("", "string");
	    stringName.appendChild(doc.createTextNode(secParam.getName()));
	    entry.appendChild(stringName);

	    // Did we override the default value for the secondary param in the example input?
	    Element stringValue = doc.createElementNS("", "string");
	    if(sampleInput != null && sampleInput.containsKey(secParam.getName())){
		stringValue.appendChild(doc.createTextNode(((MobyDataSecondaryInstance) sampleInput.get(secParam.getName())).getValue()));
	    }
	    else{  // use the default defined in Moby Central
		stringValue.appendChild(doc.createTextNode(secParam.getDefaultValue()));
	    }
	    entry.appendChild(stringValue);

	    secondaries.appendChild(entry);
	}

	activity.appendChild(doc.createElementNS(T2FLOW_NS, "annotations"));

	// boiler-plate for all processors
	processor.appendChild(getDispatchStack(doc));

	Element iterationStrategyStack = doc.createElementNS(T2FLOW_NS, "iterationStrategyStack");
	processor.appendChild(iterationStrategyStack);
	Element iteration = doc.createElementNS(T2FLOW_NS, "iteration");
	iterationStrategyStack.appendChild(iteration);
	Element strategy = doc.createElementNS(T2FLOW_NS, "strategy");
	iteration.appendChild(strategy);
	Element cross = doc.createElementNS(T2FLOW_NS, "cross");
	strategy.appendChild(cross);
	for(MobyPrimaryData inParam: service.getPrimaryInputs()){
	    Element portRef = doc.createElementNS(T2FLOW_NS, "port");
	    portRef.setAttribute("name", getPortName(inParam, true));
	    portRef.setAttribute("depth", "0");
	    cross.appendChild(portRef);
	}

	return processor;
    }

    private Registry getRegistryFromService(MobyService service) throws Exception{
	MobyServiceType serviceType = service.getServiceType();
	Registry central = null;  // check if the registry is defined anywhere in the service
	for(MobyPrimaryData inParam: service.getPrimaryInputs()){
	    if(inParam.getDataType().getRegistry() != null){
		central = inParam.getDataType().getRegistry();
		break;
	    }
	}
	if(central == null && serviceType != null && serviceType.getRegistry() != null){
	    central = serviceType.getRegistry();
	}
	if(central == null){ // must be the default registry in this case
	    if(mobyCentral != null){
		central = new Registry("any_synonym",
				       mobyCentral.getRegistryEndpoint(),
				       "any_namespace");
	    }
	    else{
		central = (new RegistriesList()).get(Registries.DEFAULT_REGISTRY_SYNONYM);
	    }
	}
	return central;
    }

    private synchronized String getListFlattenScript() throws Exception{
	if(listFlattenScript == null){
	    URL scriptURL = getClass().getClassLoader().getResource(T2FLOW_LISTFLATTEN_BEANSHELL);
	    if(scriptURL == null){
		throw new Exception("Cannot find resource " + T2FLOW_LISTFLATTEN_BEANSHELL);
	    }
	    listFlattenScript = HTMLUtils.getURLContents(scriptURL);
	}
	return listFlattenScript;
    }

    private synchronized String getPassFilterScript() throws Exception{
	if(passFilterScript == null){
	    URL scriptURL = getClass().getClassLoader().getResource(T2FLOW_PASSFILTER_BEANSHELL);
	    if(scriptURL == null){
		throw new Exception("Cannot find resource " + T2FLOW_PASSFILTER_BEANSHELL);
	    }
	    passFilterScript = HTMLUtils.getURLContents(scriptURL);
	}
	return passFilterScript;
    }

    private synchronized String getXPathFilterScript() throws Exception{
	if(xpathFilterScript == null){
	    URL scriptURL = getClass().getClassLoader().getResource(T2FLOW_XPATHFILTER_BEANSHELL);
	    if(scriptURL == null){
		throw new Exception("Cannot find resource " + T2FLOW_XPATHFILTER_BEANSHELL);
	    }
	    xpathFilterScript = HTMLUtils.getURLContents(scriptURL);
	}
	return xpathFilterScript;
    }

    private synchronized String getRegexFilterScript(int listDepth) throws Exception{
	if(regexFilterScript == null){
	    URL scriptURL = getClass().getClassLoader().getResource(T2FLOW_REGEXFILTER_BEANSHELL);
	    if(scriptURL == null){
		throw new Exception("Cannot find resource " + T2FLOW_REGEXFILTER_BEANSHELL);
	    }
	    regexFilterScript = HTMLUtils.getURLContents(scriptURL);
	}
	if(listDepth == 0){// list to scalar conversions
	    return regexFilterScript+"\nString TRUE_matchCount = \"\"+TRUE_nodelistAsXML.size();\n"+
		                     "\nString FALSE_matchCount = \"\"+FALSE_nodelistAsXML.size();\n";
        }
        else{
 	    return regexFilterScript;
        }
    }

    private synchronized Element getDispatchStack(Document newOwnerDoc) throws Exception{
	// first call, load the xml from the resource
	if(dispatchStack == null){
	    URL xmlURL = getClass().getClassLoader().getResource(T2FLOW_DISPATCHXML);
	    if(xmlURL == null){
		throw new Exception("Cannot find resource " + T2FLOW_DISPATCHXML);		
	    }
	    Document domDoc = docBuilder.parse(xmlURL.openStream());
	    dispatchStack = domDoc.getDocumentElement();
	}

	// import makes a copy
	boolean DEEP = true; //deep copy
	return (Element) newOwnerDoc.importNode(dispatchStack, DEEP);
    }

    // Describe a workflow input to Taverna
    // If the input is an Object in some namespace, the namespace and article name are fixed, and the id is used as
    // a workflow input.
    // NOTE: currently in disuse!  Replace by spreadsheet importer
    private Element createPortElement(String name, MobyDataInstance instance, MobyDataType dataType, Document doc){
	Element port = doc.createElementNS(T2FLOW_NS, "port");
	Element nameEl = doc.createElementNS(T2FLOW_NS, "name");
	nameEl.appendChild(doc.createTextNode(name));
	port.appendChild(nameEl);

	Element depth = doc.createElementNS(T2FLOW_NS, "depth");
	depth.appendChild(doc.createTextNode("0"));
	port.appendChild(depth);

	Element granularDepth = doc.createElementNS(T2FLOW_NS, "granularDepth");
	granularDepth.appendChild(doc.createTextNode("0"));
	port.appendChild(granularDepth);

	String descText = dataType.getDescription();
	if(dataType.getName().equals(MobyTags.MOBYOBJECT) && instance instanceof MobyPrimaryData){
	    MobyNamespace[] nss = ((MobyPrimaryData) instance).getNamespaces();
	    if(nss != null && nss.length > 0){
		descText = nss[0].getDescription();
	    }
	}

	// todo: deal with example values that are not simply ids
	String exampleText = ((MobyData) instance).getId();
	Element annotations = doc.createElementNS(T2FLOW_NS, "annotations");
	port.appendChild(annotations);
	annotations.appendChild(createAnnotationChain("net.sf.taverna.t2.annotation.annotationbeans.FreeTextDescription", descText, doc));
	annotations.appendChild(createAnnotationChain("net.sf.taverna.t2.annotation.annotationbeans.ExampleValue", exampleText, doc));
	return port;
    }

    public Element createAnnotationChain(String beanClass, String textValue, Document doc){
	
	Element annotation_chain = doc.createElementNS(T2FLOW_NS, "annotation_chain");
	annotation_chain.setAttribute("encoding", "xstream");

	Element AnnotationChainImpl = doc.createElementNS("", "net.sf.taverna.t2.annotation.AnnotationChainImpl");
	annotation_chain.appendChild(AnnotationChainImpl);

	Element annotationAssertions = doc.createElementNS("", "annotationAssertions");
	AnnotationChainImpl.appendChild(annotationAssertions);

	Element AnnotationAssertionImpl = doc.createElementNS("", "net.sf.taverna.t2.annotation.AnnotationAssertionImpl");
	annotationAssertions.appendChild(AnnotationAssertionImpl);

	Element annotationBean = doc.createElementNS("", "annotationBean");
	annotationBean.setAttribute("class", beanClass);
	AnnotationAssertionImpl.appendChild(annotationBean);

	Element text = doc.createElementNS("", "text");
	text.appendChild(doc.createTextNode(textValue));
	annotationBean.appendChild(text);

	Element date = doc.createElementNS("", "date");
	date.appendChild(doc.createTextNode(dateAsText()));
			 
	AnnotationAssertionImpl.appendChild(date);			 
	AnnotationAssertionImpl.appendChild(doc.createElementNS("","creators")); //blank
	AnnotationAssertionImpl.appendChild(doc.createElementNS("","curationEventList")); //blank
	
	return annotation_chain;
    }

    // Returns the current date/time as text
    private String dateAsText(){
	SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss.SSS' GMT'");
	dateFormat.setTimeZone(new SimpleTimeZone(0, "GMT"));
    	return dateFormat.format(new Date()); //now
    }

}
