package ca.ucalgary.seahawk.util;

import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.logging.*;
import java.util.regex.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;

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 org.xml.sax.InputSource;

/**
 * Contains methods for the storage and retrieval of Seahawk-specific information from
 * Moby XML payloads, specifically in the context of data provenance for programming-by-example.
 */
public class DataUtils{
    
    public static final String PI_TARGET = "seahawk";
    public static final String REGISTRY_ATTR = "registry"; // the registry against which the Moby XML and service can be validated
    public static final String SERVICEID_ATTR = "service";
    public static final String SERVICEINPUT_ATTR = "input";
    public static final String INPUTSRC_ATTR = "source";
    public static final String TEMP_FILE_PREFIX = "seahawk";
    private static final String OUTPUT_SUFFIX = ".out.xml";
    private static final String INPUT_SUFFIX = ".in.xml";
    public static DocumentBuilder docBuilder;
    private static Transformer identityTransform;
    public static XPathFactory xPathFactory;

    public static final String ARTICLE_PEERS_MODE = "article_peers";
    public static final String DATATYPE_PEERS_MODE = "datatype_peers";

    private static Logger logger = Logger.getLogger(DataUtils.class.getName());

    static{
	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);
	try{
	    docBuilder = dbf.newDocumentBuilder();
	} catch(Exception e){
	    logger.log(Level.SEVERE, "Cannot get XML parser from configuration", e);
	}

	TransformerFactory tf = TransformerFactory.newInstance();
	try{
	    identityTransform = tf.newTransformer();
	} catch(Exception e){
	    logger.log(Level.SEVERE, "Cannot get identity XSLT transform from configuration", e);
	}

	xPathFactory = XPathFactory.newInstance();
    }

    /**
     * Edit a service input document's backtracking references to have a new filter criteria.
     * This is useful when a user changes filter criteria in the midst of a PbE session.
     *
     * @param serviceResultURL the service result from which the input will be backtracked
     * @param sourceServiceResultURL input sources coming from this service result will be the target for editing
     * @param newFilter the new filter criteria for passing data to the next service
     */
    public static void updateInputFilter(URL serviceResultURL, URL sourceServiceResultURL, FilterSearch newFilter){
	Document resultDom = null;
	try{
	    resultDom = docBuilder.parse(serviceResultURL.openStream());
	} catch(Exception e){
	    logger.log(Level.SEVERE, "Cannot read " + serviceResultURL, e);
	    return;
	}

	Document inputDoc = null;
	try{
	    inputDoc = getInputDoc(resultDom); // trace back to service input via processing instructions
	} catch(Exception e){
	    logger.log(Level.SEVERE, "Cannot get input document for " + serviceResultURL, e);
	    return;
	}

	// In-place edit the DOM for the processing instructions containing the 
	// filter info if they reference input from sourceServiceResultURL 
	// (the upstream service in the invocation chain)
	List<ProcessingInstruction> piList = new Vector<ProcessingInstruction>();
	// Gather the list of Seahawk PIs
	NodeList elements = inputDoc.getDocumentElement().getElementsByTagName("*"); //all elements
	for(int i = 0; i < elements.getLength(); i++){
	    Element el = (Element) elements.item(i);
	    NodeList children = el.getChildNodes();

	    for(int j = 0; j < children.getLength(); j++){
		Node node = children.item(j);

		if(node instanceof ProcessingInstruction &&
		   ((ProcessingInstruction) node).getTarget().equals(PI_TARGET)){
		    piList.add((ProcessingInstruction) node);
		}
	    }
	}

	for(ProcessingInstruction pi: piList){
	    // Seahawk PIs have data of the form attrName="attrValue"
	    String[] attr_val = pi.getData().split("=", 2);
	    // It's an input provenance PI

	    if(attr_val.length == 2 && attr_val[0].equals(INPUTSRC_ATTR)){
		// Strip the quotes
		String attrValue = attr_val[1].replaceFirst("\\s*\"(.*)\"\\s*", "$1");
		String fields[] = attrValue.split("\t");

		if(fields[0].startsWith(sourceServiceResultURL+"#")){
		    // We have the right input source to edit the filter
		    String newProvenanceData = fields[0]+"\t"+fields[1];
		    if(newFilter != null){
			XPathOption xsel = newFilter.getSelectedXPath();
			newProvenanceData += "\t"+newFilter.getFilterRegex()+
			    "\t"+xsel.getXPath()+"\t"+xsel.getDesc()+"\t"+
			    newFilter.getCaseSensitivity()+"\t"+newFilter.getSelectionInversed();
		    }
		    pi.setData(attr_val[0]+"=\""+newProvenanceData+"\"");
		}
	    }
	}

	// Reserialize the edited service input document
	URL infileURL = null;
	try{
	    infileURL = getInputURL(resultDom);
	} catch(Exception e){
	    logger.log(Level.SEVERE, "Cannot determine input file corresponding to " + serviceResultURL, e);
	}

	if(!infileURL.getProtocol().equals("file")){
	    logger.log(Level.SEVERE, "Cannot update filter info for the service input doc " + infileURL + " (not a file URL)");
	    return;
	}
	try{
	    FileWriter fileWriter = new FileWriter(infileURL.getPath());
	    identityTransform.transform(new DOMSource(inputDoc),
					new StreamResult(fileWriter));
	    fileWriter.close();
	}catch(Exception e){
	    logger.log(Level.SEVERE, "Cannot update filter info for the service input doc " + infileURL, e);
	    return;
	}
    }

    /**
     * Gets a list of XPointers of the style /1/1/1/4/3/1 from targetInputURL that were used as 
     * input to the service call that resulted in serviceResultURL.
     * 
     * @param serviceResultURL the service response from which to backtrack the input data
     * @param targetInputURL the service response chain that we're interested in (remember that input can come from multiple docs)
     */
    public static List<String> getInputXPtrs(URL serviceResultURL, URL targetInputURL) throws Exception{
	Document resultDom = docBuilder.parse(serviceResultURL.openStream());
	
	Registry registry = getRegistry(resultDom);
	//todo: check all jobs, not just a sample
	MobyDataJob sampleJob = getInputSample(resultDom, registry);
	    
	// Trace the inputs that came from the current document 
	List<String> xPtrsReferenced = new Vector<String>();
	for(String inputName: sampleJob.keySet()){
	    MobyDataInstance sd = sampleJob.get(inputName);
	    
	    if(sd instanceof MobySecondaryData){
		continue;
	    }
	    MobyPrimaryData sampleData = (MobyPrimaryData) sd;

	    // has provenance info?
	    if(sampleData.getUserData() != null){
		// User data has the form srcURL#XpathSelectCriteria <tab> xptrActualDataSubmitted <tab> regexFilter 
		// where the filter is optional 
		String[] data = sampleData.getUserData().toString().split("\t");
		//System.err.println("Forward user data is " + sampleData.getUserData().toString());
		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() + ")", e);
		}
		// source is current doc for this service input, the form is href#xpathOfData, so check for equiv of the first part
		if(dataSrcURL != null && dataSrcURL.toString().startsWith(targetInputURL+"#")){
		    if(data.length > 1){
			xPtrsReferenced.add(data[1]);
		    }
		}
	    }
	}
	return xPtrsReferenced;
    }

    // Store the request being sent (w/ 2ndary params and all),
    // for reference (e.g. when exporting a workflow)
    protected static URL saveInputData(MobyContentInstance data) throws Exception{
 	// Create temp file.
	File temp = File.createTempFile(TEMP_FILE_PREFIX, INPUT_SUFFIX);
	
	// Delete temp file when program exits.
	temp.deleteOnExit();
	
	// Write to temp file
	FileWriter out = new FileWriter(temp);
	// In addition to the Moby XML, we want to capture any "userdata" Seahawk added to the 
	// contents.  Currently and specifically of interest is the provenance of input
	// data, whic will allow us to backtrack from a result to a service call, other earlier
	// results, etc. to reconstruct a workflow.
	out.write("<?xml version=\"1.0\"?>\n");
	out.write("<moby:MOBY xmlns:moby=\"" + MobyTags.MOBY_XML_NS + "\" " + 
		   "xmlns=\"" + MobyTags.MOBY_XML_NS + "\" >\n");
	out.write("<moby:" + MobyTags.MOBYCONTENT + ">\n");
	for(String queryName: data.keySet()){
	    out.write("    <moby:" + MobyTags.MOBYDATA + " "+MobyTags.QUERYID+"=\""+queryName+"\">");

	    MobyDataJob queryParams = data.get(queryName);
	    for(String paramName: queryParams.keySet()){
		MobyDataInstance queryObject = queryParams.get(paramName);

		int oldXmlMode = queryObject.getXmlMode();
                if(oldXmlMode != MobyDataInstance.SERVICE_XML_MODE){
                    queryObject.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
                }

		if(queryObject instanceof MobyDataObject){
		    // This line should be replaced with a named field
		    out.write("       <"+MobyTags.SIMPLE+" "+MobyTags.ARTICLENAME+"='" + paramName.replaceAll("'","&apos;") + "'>");
		    if(queryObject.getUserData() != null){
			out.write("         <?"+PI_TARGET+" "+INPUTSRC_ATTR+"=\""+queryObject.getUserData().toString()+"\"?>");
		    }
		    out.write(queryObject.toXML()+ "</"+MobyTags.SIMPLE+">");
		}
		else if(queryObject instanceof MobyDataObjectSet){
		    String oldName = queryObject.getName();
		    queryObject.setName(paramName);
		    out.write(queryObject.toXML());
		    queryObject.setName(oldName);
		    if(queryObject.getUserData() != null){
			out.write("         <?"+PI_TARGET+" "+INPUTSRC_ATTR+"=\""+queryObject.getUserData().toString()+"\"?>");
		    }
		}
		//  a secondary input parameter
		else{
		    out.write(queryObject.toXML());
		}
                
                // Restore the old XML mode setting if not service mode
                if(oldXmlMode != MobyDataInstance.SERVICE_XML_MODE){
                    queryObject.setXmlMode(oldXmlMode);
                }
	    }

	    out.write("    </moby:" + MobyTags.MOBYDATA + ">");	    
	}

	out.write("          </moby:" + MobyTags.MOBYCONTENT + ">\n");
	out.write("\n</moby:MOBY>\n");

	out.close();

	return temp.toURI().toURL();
    }

    public static URL saveOutputData(MobyRequestEvent mre) throws Exception{
	return saveOutputData(mre.getContentsXML().toString(), 
			      mre.getService(), 
			      saveInputData(mre.getSourceInput()), 
			      mre.getSource().getCentralImpl().getRegistryEndpoint());
    }

    public static URL saveOutputData(MobyContentInstance outputData, MobyService payloadSource, 
			      MobyContentInstance inputData, String registryEndpoint) throws Exception{
	ByteArrayOutputStream xml = new ByteArrayOutputStream();
	MobyDataUtils.toXMLDocument(xml, outputData);
	return saveOutputData(xml.toString(), payloadSource, saveInputData(inputData), registryEndpoint);
    }

    public static URL saveOutputData(String output, MobyService payloadSource, URL inputURL, String registryEndpoint) throws Exception{
	// Create temp file.
	File temp = File.createTempFile(TEMP_FILE_PREFIX, OUTPUT_SUFFIX);
	
	// Delete temp file when program exits.
	temp.deleteOnExit();

	// Edit the XML response to include an XML processing instruction (just after the <?xml?> one)
	// that says what service was run to generate the output.
	String contents = output.replaceFirst("\\?>",
					      "?>\n<?"+PI_TARGET+" "+SERVICEID_ATTR+"=\""+payloadSource.getLSID()+"\"?>\n"+
	// record input doc as well
					      "<?"+PI_TARGET+" "+REGISTRY_ATTR+"=\""+registryEndpoint+"\"?>\n"+
					      "<?"+PI_TARGET+" "+SERVICEINPUT_ATTR+"=\""+inputURL+"\"?>\n");
	// Write to temp file
	FileWriter out = new FileWriter(temp);
	out.write(contents);
	out.close();

	return temp.toURI().toURL();
    }

    public static URL getInputURL(Document responseDom) throws Exception{
	String inputAddr = getSeahawkAttrFromDoc(responseDom, SERVICEINPUT_ATTR);
	if(inputAddr == null){
	    return null;
	}
	URL inputURL = null;
	try{
	    return new URL(inputAddr);
	} catch(Exception e){
	    throw new Exception("Could not parse " + SERVICEINPUT_ATTR + 
				" processing instruction attribute (" +
				inputAddr + ") into a URL");
	}
    }

    public static Document getInputDoc(Document responseDom) throws Exception{
	URL inputURL = getInputURL(responseDom);
	if(inputURL == null){
	    return null;
	}
	try{
	    return docBuilder.parse(getInputURL(responseDom).openStream());
	} catch(org.xml.sax.SAXException saxe){
	    throw new Exception("The service input XML data could not be parsed (not well-formed): " + saxe);
	} catch(java.io.IOException ioe){
	    throw new Exception("The service input XML data could not be loaded (I/O problem): " + ioe);
	}
    }

    /** 
     * Backtrack from a service result to the input doc sent to the service
     *
     * @param responseDom the XML DOM for the answer from a Moby Web service called in Seahawk
     */
    public static MobyDataJob getInputSample(Document responseDom, Registry registry) throws Exception{
	return getInputSample(responseDom, null, registry);
    }

    /**
     * Same as two-arg, but retrieves a specific job's input (or null if not a job name found in the doc).
     */
    public static MobyDataJob getInputSample(Document responseDom, String jobName, Registry registry) throws Exception{
	Document inputDoc = getInputDoc(responseDom);
	if(inputDoc == null){
	    return null;
	}
	MobyContentInstance inputPayload = MobyDataUtils.fromXMLDocument(inputDoc.getDocumentElement(),
									 registry);
	if(inputPayload == null || inputPayload.size() == 0){
	    throw new Exception("Could not parse Moby input message from the given DOM");
	}
	addUserDataToPayload(inputPayload, inputDoc);

	MobyContentInstance outputPayload = MobyDataUtils.fromXMLDocument(responseDom.getDocumentElement(),
									  registry);
	if(outputPayload == null || outputPayload.size() == 0){
	    throw new Exception("Could not parse Moby output message from the given DOM");
	}

	if(jobName != null){
	    return inputPayload.get(jobName);
	}
	// Pick a job that produced output, if given the choice of more that one
	for(String jobID: inputPayload.keySet()){
	    if(outputPayload.containsKey(jobID) &&
	       outputPayload.get(jobID).size() > 0){
		return inputPayload.get(jobID);
	    }
	}
	return (MobyDataJob) inputPayload.values().iterator().next();
    }

    /**
     * Tacks the data provenance info saved as processing instructions during saveInputData()
     * back into the MobyContentInstance loaded from file using the usual, 
     * customization-oblivious MobyDataUtils.fromXMLDocument().
     */
    public static void addUserDataToPayload(MobyContentInstance contentInstance, Document sourceDoc) throws Exception{
	
	// Search for the PI provenance info (tagged as INPUTSRC_ATTR="val")
	NodeList jobs = ((Element) sourceDoc.getDocumentElement().getElementsByTagNameNS(MobyTags.MOBY_XML_NS, MobyTags.MOBYCONTENT).item(0)).getElementsByTagNameNS(MobyTags.MOBY_XML_NS, MobyTags.MOBYDATA);
	for(int i = 0; i < jobs.getLength(); i++){
	    String jobName = getAttribute((Element) jobs.item(i), MobyTags.QUERYID);
	    if(jobName == null){
		throw new Exception("The provided DOM does not appear to be proper Moby XML: no " + 
				    MobyTags.QUERYID + " attribute was found for a " + MobyTags.MOBYDATA + " tag");
	    }
	    NodeList jobData = ((Element) jobs.item(i)).getChildNodes();

	    for(int j = 0; j < jobData.getLength(); j++){
		if(!(jobData.item(j) instanceof Element)){
		    continue;
		}
		Element param = (Element) jobData.item(j);
		String paramName = null;

		MobyDataInstance mobyDataInstance = null;
		ProcessingInstruction pi = null;
		if(param.getLocalName().equals(MobyTags.SIMPLE)){
		    paramName = getAttribute(param, MobyTags.ARTICLENAME);
		    if(paramName == null){
			throw new Exception("The provided DOM does not appear to be proper Moby XML: no " + 
					    MobyTags.ARTICLENAME + " attribute was found for a " + MobyTags.SIMPLE + " tag");
		    }
		    NodeList simpleChildren = param.getChildNodes();
		    for(int k = 0; k < simpleChildren.getLength(); k++){
			if(simpleChildren.item(k) instanceof ProcessingInstruction &&
			   ((ProcessingInstruction) simpleChildren.item(k)).getTarget().equals(PI_TARGET)){
			    pi = (ProcessingInstruction) simpleChildren.item(k);
			    // hope there isn't more than one processing instruction for 
			    // this app here in the DOM (i.e. using PI_TARGET) or an exception is thrown later
			    break;  
			}
		    }
		}
		else if(param.getLocalName().equals(MobyTags.COLLECTION)){
		    // associated pi comes after a collection tag, but before the next element
		    paramName = getAttribute(param, MobyTags.ARTICLENAME);
		    for(int k = 1; j+k < jobData.getLength(); k++){
			if(jobData.item(j+k) instanceof Element){
			    break;
			}
			if(jobData.item(j+k) instanceof ProcessingInstruction){
			    pi = (ProcessingInstruction) jobData.item(j+k);
			    break;
			}
		    }
		}
		else{
		    continue; //secondary param or PI itself
		}
		// Is a PI, and is one specific to this app
		if(pi != null){
		    String[] attr_val = pi.getData().split("=", 2);
		    if(attr_val.length == 2 && attr_val[0].equals(INPUTSRC_ATTR)){
			// strip the quotes
			String attrValue = attr_val[1].replaceFirst("\\s*\"(.*)\"\\s*", "$1");
			if(!contentInstance.containsKey(jobName)){
			    throw new Exception("DOM and content instance provided seem out of " +
						"sync: the instance has no job named '" + jobName + "'");
			}
			if(!contentInstance.get(jobName).containsKey(paramName)){
			    throw new Exception("DOM and content instance provided seem out of " +
						"sync: the instance job '" + jobName + 
						"' has no parameter '" + paramName + "'");
			}
			//System.err.println("Setting user data val to " + attrValue + " for " + paramName);
      			contentInstance.get(jobName).get(paramName).setUserData(attrValue);		    
		    }
		    else{
			throw new Exception("Expected processing instruction (target " + PI_TARGET + 
					    ") with data of form " + INPUTSRC_ATTR + "=\"...\", but " +
					    "found " + pi.getData());
		    }
		}
	    }
	}	
    }

    /**
     * Loads targetURL, minus the nodes specified by the xpath keys in filteredXPtrs (map values are not currently used).
     */
    public static void filterNodes(Node rootNode, Map<String,String> filteredXPtrs){
	 
	if(rootNode == null){
	    logger.log(Level.SEVERE, "Error: Could not get MOBY document as DOM (empty or malformed document?)");
	    return;
	}
	
	// Remove the filtered data from the doc before deserializing to a moby data instance
	// That way the request actually reflects what is shown on the screen to the user.
	if(filteredXPtrs != null){
	    // Mark and sweep (if we just deleted as we found nodes, the XPointers could be 
	    // wrong as they refer to ordinality)
	    Vector<Node> nodesToDelete = new Vector<Node>();
	    for(String xptr: filteredXPtrs.keySet()){
		nodesToDelete.add(XPointerResolver.getNodeFromXPointer(rootNode, xptr));
	    }

	    // Delete
	    for(Node nodeToDelete: nodesToDelete){
		Node parent = nodeToDelete.getParentNode();
		parent.removeChild(nodeToDelete);
		// Get rid of Simple parents of deleted nodes, because empty Simple
		// tags are not okay in Moby (causes parsing error)
		if(MobyTags.SIMPLE.equals(parent.getLocalName())){
		    parent.getParentNode().removeChild(parent);
		}
	    }
	}
    }

    /**
     * Get the data instance object associated with a given XPointer in a Moby XML doc.
     *
     * @param targetURL of the form URLpath#/1/1/2/1 where /1/1/2/1 is an XPointer, or an standard xpath, to the part of the Moby XML doc to deserialize
     * @param filteredXPtrs a map of the xpointers to data that should NOT be deserialized if children of the targetURL XPointer
     * @param docFilter the current filter applied to the document in targetURL
     */
     public static MobyDataInstance loadMobyDataFromXPointer(URL targetURL, Map<String,String> filteredXPtrs, 
							     FilterSearch docFilter){
	 
	 URL currentURL = null;
	 try{currentURL = new URL(targetURL.toString().replaceAll("#"+targetURL.getRef(), ""));}
	 catch(Exception e){
	     logger.log(Level.SEVERE, "Couldn't extract referenceless URL from " + targetURL, e);
	 }
	 
	 // Build the DOM
	 Document domDoc = null;
	 try{
	     domDoc = docBuilder.parse(targetURL.openStream());
	 } catch(org.xml.sax.SAXException saxe){
	     logger.log(Level.SEVERE, "The document defining the MOBY data " +
			"could not be parsed", saxe);
	     return null;
	 } catch(java.io.IOException ioe){
	     logger.log(Level.SEVERE, "The document defining the MOBY data " +
			" could not be read (from " + targetURL + ")", ioe);
	     return null;
	 }
	 
	 return loadMobyDataFromXPointer(currentURL, domDoc, targetURL.getRef(), filteredXPtrs, docFilter);
    }
    
    public static MobyDataInstance loadMobyDataFromXPointer(URL docURL, Document domDoc, String targetXptr, 
							    Map<String,String> filteredXPtrs, FilterSearch docFilter){
	// Are we dealing with a simple object or a complex one?
	MobyDataInstance mobyData = null;
	
	// Find the DOM fragment corresponding to the MOBY ID anchor specified
	// in the link URL by using an XPath statement on the source MOBY doc
	
	// A child Xpointer is of the form /1/2/1/1, specifying the DOM child 
	// descent path from the root node to get to the target node. 
	Element mobyObject = (Element) XPointerResolver.getNodeFromXPointer(domDoc, targetXptr);
	
	// Does an in-place edit of domDoc
	// Check if the returned data was supposed to be filtered.  If so, don't filter.
	if(filteredXPtrs != null && !filteredXPtrs.isEmpty() && !filteredXPtrs.containsKey(targetXptr)){
	    // Otherwise filter, so child elements of the data to return reflect the current filter conditions 
	    // (e.g. a collection may be filtered to just a subset based on namespace, so retrieving the collection should only 
	    // return that subset of children).
	    domDoc = (Document) domDoc.cloneNode(true); // true == deep
	    filterNodes(domDoc, filteredXPtrs);
	}

	// Create the Java MOBY API object based on the linked document DOM fragment
	try{
	    MobyDataInstance mdi = MobyDataObject.createInstanceFromDOM(mobyObject, SeahawkOptions.getRegistry());
	    if(mdi instanceof MobyDataObject){
		mobyData = mdi;
	    }
	    else if(mdi instanceof MobyDataObjectSet){
		mobyData = mdi;
	    }
	    else{
		logger.log(Level.WARNING, "Error: Moby data instance retrieved with XPath " + targetXptr +
			   " was not a primary MOBY input object as expected");
		return null;
	    }

	    // To avoid misinterpretation of primitive datatypes as Objects with any namespace in queries for
	    // services, add a token namespace to get reasonable service results.
	    if(mobyData instanceof MobyDataObject && ((MobyDataObject) mobyData).getPrimaryNamespace() == null){
		((MobyDataObject) mobyData).setPrimaryNamespace(new MobyNamespace("unknown"));
	    }

	    // For workflow creation, etc. associate the potential input data with its source generalized XPath
	    // Also, if there is currently a filter on the doc, store it too as useful info for workflow creation.
	    setUserData(mobyData, docURL, elementInContextToNameBasedXPath(mobyObject), 
			targetXptr, docFilter);
	}
	catch(MobyException mobye){  // Logic error
	    logger.log(Level.SEVERE, "Error: Could not construct Moby data instance from document fragment: ", 
		       mobye);
	}
	
	return mobyData;
    }

    /**
     * Generalizes the element instance into an XPath retrieving it and all 
     * similarly nested elements (based on traversing the parent nodes and prepending their names)
     */
    public static String elementInContextToNameBasedXPath(Element targetElement){
	String xpath = "";
	
	for(Element currentElement = targetElement; 
	    currentElement != null;
	    currentElement = (Element) currentElement.getParentNode()){
	    String elName = currentElement.getLocalName();
	    // top level parameters' names are important
	    String articleName = currentElement.getAttributeNS(MobyTags.MOBY_XML_NS, MobyTags.ARTICLENAME);
	    if(articleName == null || articleName.trim().length() == 0){
		articleName = currentElement.getAttribute(MobyTags.ARTICLENAME);
	    }

	    if(elName.equals(MobyTags.SIMPLE)){
		xpath = "/"+MobyTags.SIMPLE+"[@"+MobyTags.ARTICLENAME + " = '" +
		    articleName+"']" +xpath;		
	    }

	    else if(elName.equals(MobyTags.COLLECTION)){
		xpath = "/"+MobyTags.COLLECTION+"[@"+MobyTags.ARTICLENAME + " = '" +
		    articleName +"']"+xpath;
	    }
	    else if(elName.equals(MobyTags.MOBYOBJECT)){
		// Obviously, assume a base object is interesting based on its namespace
		xpath = "/*[@"+MobyTags.OBJ_NAMESPACE + " = '" +
		    currentElement.getAttribute(MobyTags.OBJ_NAMESPACE)+"']" + xpath;
	    }
//	    else if(elName.equals(MobyTags.MOBYFLOAT) ||
//		    elName.equals(MobyTags.MOBYSTRING) ||
//		    elName.equals(MobyTags.MOBYBOOLEAN) ||
//		    elName.equals(MobyTags.MOBYINTEGER) ||
//		    elName.equals(MobyTags.MOBYDATETIME)){
		// Assume that it isn't the primitive type, but rather the articleName
		// that makes it appropriate to use.
//		xpath = "/*[@"+MobyTags.ARTICLENAME + " = '" +
//		    articleName+"']" + xpath;
//	    }
	    else{
		// It's a complex type.  Assume the type is why we are picking it.
		// TODO: In future we may want to check if a parent type is allowed in 
		// this slot, and generalize to that.  
		// Also, account for namespace if present?
		xpath = "/"+elName + xpath;
	    }
	    if(!(currentElement.getParentNode() instanceof Element)){
		break; // reached document node
	    }
	}
	
	return xpath;
    }

    /**
     * Record the PbE pertinent info about the data to the Moby data object so it can be tracked through the rest of the app.
     */
    public static void setUserData(MobyDataInstance mobyData, 
				   URL srcURL,
				   String selectionXPath, 
				   String dataSrcXPtr, 
				   FilterSearch fs){
	StringBuilder userData = new StringBuilder();
	userData.append(srcURL + "#" + selectionXPath); //generalization of criteria for data selection
	userData.append("\t" + dataSrcXPtr); //pointer to the exact data being used
	
	if(fs != null && fs.getFilterRegex().length() > 0){
	    XPathOption xsel = fs.getSelectedXPath();
	    userData.append("\t"+fs.getFilterRegex()+"\t"+xsel.getXPath()+"\t"+xsel.getDesc()+"\t"+
			    fs.getCaseSensitivity()+"\t"+fs.getSelectionInversed());
	}
	mobyData.setUserData(userData.toString());
    }

    /**
     * Record a condition for data acceptability, 
     * another service call with an output filter criteria. i.e. records if(f1(x) matches f1's output filter){...}
     */
    public static void addUserData(MobyDataInstance mdi, URL conditionalOutputURL, FilterSearch filter){
	String filterSpec = "";
	if(filter != null && filter.getFilterRegex().length() > 0){
	    XPathOption xsel = filter.getSelectedXPath();
	    filterSpec = "\t"+filter.getFilterRegex()+"\t"+xsel.getXPath()+"\t"+xsel.getDesc()+"\t"+
		filter.getCaseSensitivity()+"\t"+filter.getSelectionInversed();
	}
	if(mdi.getUserData() != null){
	    mdi.setUserData(mdi.getUserData().toString()+"\t"+conditionalOutputURL+filterSpec);
	}
	else{
	    mdi.setUserData(conditionalOutputURL.toString()+filterSpec);
	}
    }

    /**
     * Replace the selection xpath info in the data instance
     */
    public static void replaceUserData(MobyDataInstance mdi, String selectionXPath){
    }

    /**
     * Retrieves processing instructions embedded in Moby XML docs if they have the for <?seahawk attr="val"?>
     */
    public static String getSeahawkAttrFromDoc(URL docURL, String attrName) throws Exception{ 
        // Check a processing instruction we planted in the response before we saved it
        Document domDoc = null;
	try{
	    domDoc = docBuilder.parse(docURL.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);
	}
        return getSeahawkAttrFromDoc(domDoc, attrName);
    }

    public static String getSeahawkAttrFromDoc(Document mobyXmlDoc, String attrName) throws Exception{
	// Search for the PI
	NodeList topLevelChildren = mobyXmlDoc.getChildNodes();
	String attrValue = null;
	for(int i = 0; i < topLevelChildren.getLength(); i++){
	    if(topLevelChildren.item(i) instanceof ProcessingInstruction){
		ProcessingInstruction pi = (ProcessingInstruction) topLevelChildren.item(i);
		if(pi.getTarget().equals(PI_TARGET)){
		    String[] attr_val = pi.getData().split("=", 2);
		    if(attr_val.length == 2){
			if(attr_val[0].equals(attrName)){
			    // strip the quotes
			    attrValue = attr_val[1].replaceFirst("\\s*\"(.*)\"\\s*", "$1");
			    break;
			}
		    }
		    else{
			throw new Exception("Expected processing instruction (target " + PI_TARGET + 
					    ") with data of form attrName=\"...\", but " +
					    "found " + pi.getData());
		    }
		}
	    }
	}

	return attrValue;
    }

    /**
     * Find out what the service was that created the given URL.
     * 
     * @param responseURL the service response whose origin we should trace
     */
    public static MobyService getService(URL responseURL) throws Exception{
        String serviceLSID = getSeahawkAttrFromDoc(responseURL, SERVICEID_ATTR);
	return serviceLSID == null ? null : MobyService.getService(serviceLSID);
    }
    public static MobyService getService(Document responseDoc) throws Exception{
        String serviceLSID = getSeahawkAttrFromDoc(responseDoc, SERVICEID_ATTR);
	return serviceLSID == null ? null : MobyService.getService(serviceLSID);
    }

    private static String getAttribute(Element e, String attrName){
	String attrValue = e.getAttributeNS(MobyTags.MOBY_XML_NS, attrName);
	if(attrValue != null && attrValue.length() > 0){
	    return attrValue;
	}
	attrValue = e.getAttributeNS("", attrName);
	if(attrValue != null && attrValue.length() > 0){
	    return attrValue;
	}
	return e.getAttribute(attrName);
    }  

    /**
     * Find out which registry the service that created the reponse is registered in.
     * 
     * @param responseDoc the service response whose origin we should trace
     */
    public static Registry getRegistry(Document responseDoc) throws Exception{
        String endpointURL = getSeahawkAttrFromDoc(responseDoc, REGISTRY_ATTR);
	return new Registry("any_synonym",
			    endpointURL == null ? CentralImpl.getDefaultURL() : endpointURL,
			    "any_namespace");
    }
      
    // Recursively ascend the DOM tree and find out our place in its branching structure
    public static String getXPtr(Node n){
	if(n == null || n instanceof Document){
	    return "";
	}
	Node parent = n.getParentNode();
        if(parent == null && n instanceof Attr){
	    parent = ((Attr) n).getOwnerElement();
	}

	NodeList children = parent.getChildNodes();
	int nonElementCnt = 0;
	for(int i = 0; i < children.getLength(); i++){
	    if(!(children.item(i) instanceof Element)){
		nonElementCnt++; 
		continue;
	    }
	    if(n == children.item(i)){
		return getXPtr(parent)+"/"+(i-nonElementCnt+1);
	    }
	}
	return null;
    }

    /**
     * If any moby object member is in a HAS relationship, add it to the list of filterable items
     */
    public static Boolean addHASXPtrs(List<String> filterList, Element object){
	String tagName = object.getLocalName();
	boolean isContainer = false;
	MobyDataType mobyDataType = null;	
	if(tagName.equals(MobyTags.MOBYDATA) || tagName.equals(MobyTags.COLLECTION) || tagName.equals(MobyTags.SIMPLE) ||
	   tagName.equals(MobyTags.CROSSREFERENCE)){
	    isContainer = true;
	}
	else{
	    mobyDataType = MobyDataType.getDataType(tagName, SeahawkOptions.getRegistry());
	    if(mobyDataType == null){
		logger.log(Level.WARNING,
                           "Found datatype unknown to the registry ("+object.getLocalName()+")");
		return Boolean.FALSE;
	    }
	}

	Boolean hasHAS = Boolean.FALSE; // does the object have a member with a HAS relationship?
	NodeList members = object.getChildNodes();
	for(int i = 0; i < members.getLength(); i++){
	    if(!(members.item(i) instanceof Element)){
		continue;
	    }

	    Element member = (Element) members.item(i);
	    addHASXPtrs(filterList, member);

	    if(!isContainer){
		String memberName = member.getAttribute(MobyTags.ARTICLENAME);
		MobyRelationship membership = mobyDataType.getChild(memberName);
		//System.err.println("Relationship for " + tagName  + " member " + memberName + 
		//		   " is " + (membership == null ? null : membership.getRelationshipType()));
		if(membership != null && membership.getRelationshipType() == Central.iHAS){
		    filterList.add(getXPtr(member));
		    if(!hasHAS){
			hasHAS = Boolean.TRUE;
		    }
		}
	    }
	}
	return hasHAS;
    }

    /**
     * For one-off filtering of a doc. Populates xPtrsToFilter, and returns the 
     * whole doc which you can manipulate yourself (e.g. removing the nodes identified).
     */
    public static Document findFilteredNodes(URL targetURL, FilterSearch filter, 
                                             Map<String,String> xPtrsToFilter){
        Document unfilteredDoc = null;
	try{
	    unfilteredDoc = docBuilder.parse(targetURL.openStream());
	} catch(Exception e){
	    logger.log(Level.SEVERE,
                       "Could not parse Moby document " + targetURL, e);
	    return null;
	}
	findFilteredNodes(unfilteredDoc, filter, null, xPtrsToFilter, null, null, null, true);
	return unfilteredDoc;
    }

    /**
     * Returns a list of xptrs to filterable nodes in the targetDoc, an populates the list of 
     * xptrs that should be filtered according to the provided filter.  In-place edited fields are
     * xPtrsToFilter, jobXPtrs, currentSelectedData, and currentSelectionXPath 
     * (for caching efficiency on multiple consecutive 
     * calls to this method, such as a live typed search in the UI).  The latter three can be null if no caching is desired.
     */
    public static List<String> findFilteredNodes(Document targetDoc, FilterSearch filter, List<String> filterableXPtrs,
		    		                 Map<String,String> xPtrsToFilter, Map<String,Boolean> jobXPtrs,
                                                 MutableNodeList currentSelectedData, StringBuffer currentSelectionXPath, 
						 boolean apply){
	xPtrsToFilter.clear();

	// Filter out any moby jobs, parameters, collection members or HAS members that aren't in the matchingNodes
	if(filterableXPtrs == null){
	    if(jobXPtrs == null){
		jobXPtrs = new HashMap<String,Boolean>();
            }
            else{
                jobXPtrs.clear();
            }
	    filterableXPtrs = new Vector<String>();
	    NodeList nodes = targetDoc.getElementsByTagNameNS(MobyTags.MOBY_XML_NS, MobyTags.MOBYDATA);
	    for(int i = 0; i < nodes.getLength(); i++){
		String jobXPtr = getXPtr(nodes.item(i));		
		filterableXPtrs.add(jobXPtr);
		jobXPtrs.put(jobXPtr, addHASXPtrs(filterableXPtrs, (Element) nodes.item(i)));
	    }
		
	    // Collections
	    nodes = targetDoc.getElementsByTagNameNS(MobyTags.MOBY_XML_NS, MobyTags.COLLECTION);
	    for(int i = 0; i < nodes.getLength(); i++){
		String collectionXPtr = getXPtr(nodes.item(i));
		NodeList collectionMembers = nodes.item(i).getChildNodes();
		int nonElementCnt = 0;
		for(int j = 0; j < collectionMembers.getLength(); j++){		    
		    if(!(collectionMembers.item(j) instanceof Element)){
			nonElementCnt++;
			continue;
		    }
		    filterableXPtrs.add(collectionXPtr+"/"+(j-nonElementCnt+1)+"/1");
		}
	    }
	}

	if(!apply || filter == null || filter.getFilterRegex().toString().length() == 0){
	    return filterableXPtrs;
	}
	// Otherwise make a list of xpointers to data that should be filtered
	XPathOption xsel = filter.getSelectedXPath();
	
	// Find the applicable DOM nodes if not yet found, or if selection criteria have changed
	if(currentSelectedData == null || currentSelectedData.isEmpty() || 
           currentSelectionXPath == null || !currentSelectionXPath.toString().equals(xsel.getXPath())){
            if(currentSelectionXPath == null){
               currentSelectionXPath = new StringBuffer(xsel.getXPath());
            }
	    else{
               currentSelectionXPath.replace(0, currentSelectionXPath.length(), xsel.getXPath());
            }
	    try{
		if(currentSelectedData == null){
                    currentSelectedData = new MutableNodeList();
                }
                else{
                    currentSelectedData.clear();
                }
		currentSelectedData.addAll((NodeList) xPathFactory.newXPath().evaluate(currentSelectionXPath.toString(), 
                                                                                       targetDoc, 
                                                                                       XPathConstants.NODESET)); 
	    } catch(Exception e){
		logger.log(Level.SEVERE, 
                           "Could not evaluate XPath (" + xsel.getXPath() + ")", e);
		return filterableXPtrs;
	    }
	    //	    System.err.println("There are " + currentSelectedData.getLength() + 
	    //		       " items selected in the document using XPath " + currentSelectionXPath);
	}
	
	// Find just the data subset also matching the regex
	Map<String,Boolean> matchingXPtrs = new LinkedHashMap<String,Boolean>();
	// Build the FSA only once, for efficiency
	Pattern regex = Pattern.compile(filter.getFilterRegex().toString(), 
                                        Pattern.MULTILINE | Pattern.DOTALL | 
                                         (filter.getCaseSensitivity() ? 0 : Pattern.CASE_INSENSITIVE));
	for(int i = 0; i < currentSelectedData.getLength(); i++){
	    Node node = currentSelectedData.item(i);	    
	    if(node instanceof Element){
		String elementXPtr = getXPtr(node);
		if(matchingXPtrs.containsKey(elementXPtr) &&
		   matchingXPtrs.get(elementXPtr).booleanValue()){
		    	continue;  // already true, no 
                    }
		else if(regex.matcher(((Element) node).getTextContent()).find()){
		    matchingXPtrs.put(elementXPtr, Boolean.TRUE);
		    //System.err.println("Adding " + elementXPtr + " as " + matchingXPtrs.get(elementXPtr));
		}
		else{
		    matchingXPtrs.put(elementXPtr, Boolean.FALSE);
		    //System.err.println("Adding " + elementXPtr + " as " + matchingXPtrs.get(elementXPtr));
		}
	    }
	    else if(node instanceof Attr){
		String attrParentXPtr = getXPtr(((Attr) node).getOwnerElement());
		if(matchingXPtrs.containsKey(attrParentXPtr) &&
	 	   matchingXPtrs.get(attrParentXPtr).booleanValue()){
		    continue;
                }
		else if(regex.matcher(((Attr) node).getValue()).find()){
		    // Mark the element to which the attribute belongs
		    matchingXPtrs.put(attrParentXPtr, Boolean.TRUE);
		    //System.err.println("Adding " + attrParentXPtr + " attr parent as " + matchingXPtrs.get(attrParentXPtr));
		}
		// so false doesn't override true for multi-attr elements
		else if(!matchingXPtrs.containsKey(attrParentXPtr)){ 
		    matchingXPtrs.put(attrParentXPtr, Boolean.FALSE);
		    //System.err.println("Adding " + attrParentXPtr + " attr parent as " + matchingXPtrs.get(attrParentXPtr));
		}
		
	    }
	    else{
		logger.log(Level.WARNING, 
			   "Found filter xpath result item that was not an Element or Attribute as expected ("+
			    node.getClass().getName()+")");
	    }
	}
	
	boolean inversed = filter.getSelectionInversed();
	Map<String,Boolean> parentMatchingXPtrs = new LinkedHashMap<String,Boolean>();
	for(String currXPtr: filterableXPtrs){
	    for(Map.Entry<String,Boolean> matchingXPtr: matchingXPtrs.entrySet()){
		if(matchingXPtr.getKey().startsWith(currXPtr+"/")){ // a parent of the matching data
		    // No positive example yet?
		    if(!parentMatchingXPtrs.containsKey(currXPtr) || 
		       !inversed && !parentMatchingXPtrs.get(currXPtr).booleanValue() ||
		       inversed && parentMatchingXPtrs.get(currXPtr).booleanValue()){
			//System.err.println("Adding "+ matchingXPtr.getValue() + " for " + currXPtr);
			parentMatchingXPtrs.put(currXPtr, matchingXPtr.getValue());
		    }
		}
	    }
	}

	matchingXPtrs.putAll(parentMatchingXPtrs);
	for(String currXPtr: matchingXPtrs.keySet()){
	    // Is part of the selection criteria, but doesn't match the regex
	    boolean shouldFilter = false;
	    if(!matchingXPtrs.get(currXPtr).booleanValue() &&
	       // special condition: if "entire response" xpath is given, filter only at the job level
	       (!currentSelectionXPath.toString().equals(FilterSearch.SELECT_ALL_XPATH) || jobXPtrs.containsKey(currXPtr))){
		shouldFilter = true;
	    }
	    if(!inversed && shouldFilter ||
	       inversed && !shouldFilter){ 
		xPtrsToFilter.put(currXPtr, "whatever");
	    }	    
	}

	//todo: filter objects without the HAS field at all...
	return filterableXPtrs;
    }

    /**
     * Create a temp file that populates the MobyContentInstance from the sample data
     * for all missing fields in peerJobs.  This is used to populate a bunch of jobs
     * at once, iterating over some list of values popped into the peerJobs payload.
     * Used after service wrapping demo to call new service fopr all demo input peers.
     */
    public static URL createServiceInputFileForPeers(MobyContentInstance peerJobs, MobyDataJob sampleJob) throws Exception{
	for(MobyDataJob peerJob: peerJobs.values()){
	    for(String paramName: sampleJob.keySet()){
		if(!peerJob.containsKey(paramName)){
		    peerJob.put(paramName, sampleJob.get(paramName));
		}
	    }
	}
	
	return saveInputData(peerJobs);
    }

    /**
     * We need to enumerate the possible peer-sets for the selected data item.  Is the user interested in
     * items in the same namespace/data type, or same article name?
     */
    public static NodeList getPeerElements(Document doc, MobyDataInstance mobyData, 
					   Map<String,String> xPtrsToFilter, String peerMode){
        if(!(mobyData instanceof MobyPrimaryData)){
            logger.log(Level.WARNING, "Tried to get peers of data that was not an instanceof MobyPrimaryData (was " + 
                                      mobyData.getClass().getName()+ ")");
	    return null;
	}
        MobyDataType dataTypeTemplate = ((MobyPrimaryData) mobyData).getDataType();

	NodeList peerElements = null;
        String peerGroupXPath = null;
	Node contextNode = null;
	if(mobyData.getUserData() != null){
	    String[] userData = ((String) mobyData.getUserData()).split("\t");
	    // first arg is url#nameBasedXPath 
	    peerGroupXPath = userData[0].split("#", 2)[1];
	    contextNode = XPointerResolver.getNodeFromXPointer(doc, userData[1]);
	    System.err.println("Loaded context node " + userData[1] + " as " + contextNode);
        }
        else{
	    // Fallback is to get all nodes with the same name...
            // warning: need to set this in the userdata sent to the job, to jibe with taverna workflow export selection of nodes!
	    logger.log(Level.WARNING, "No UserData for data instance, falling back to all peer elements of the same datatype");
            peerGroupXPath = "//"+dataTypeTemplate.getName();
	}

        // just look at data type tag, regardless of nesting
        if(DATATYPE_PEERS_MODE.equals(peerMode)){
            peerGroupXPath = peerGroupXPath.replaceFirst("^.+/", "//"); 
        }
        else if(ARTICLE_PEERS_MODE.equals(peerMode)){ 
            String xpathArticleCondition = "[@"+MobyTags.ARTICLENAME + "='" + mobyData.getName() +"']";
            // Pass back modified selection xpath for userdata (enables PBE to capture the additonal semantics)
            // This is done by replacing first tab delimited field url#selectionXPath with url#newXPath
            String newUserData = mobyData.getUserData().toString().replaceFirst("^(.*?)#.*?(?=\\t)", 
                                                                                "$1#"+peerGroupXPath+xpathArticleCondition);
            mobyData.setUserData(newUserData);

            // keep from job article name down
            //peerGroupXPath = peerGroupXPath.replaceFirst("^.*?/(Collection|Simple)", "//*");
	    // we want to go by articleName too to catch the full semantics of the peer group definition
            peerGroupXPath += xpathArticleCondition;
        }
        else{
            logger.log(Level.SEVERE, "Got unknown mode for peer selection, aborting (mode was "+peerMode+")");
            return null;
        }
        // For xpaths given above, MOBY namespace is problem in XPath evaluation. 
        // solution: eliminate Moby envelope path parts: full path doesn't resolve in xpath for unknown reasons
        //.replaceAll("/(MOBY|mobyContent|mobyData|Collection|Simple)","/moby:$1"); //attempt 1
        peerGroupXPath = peerGroupXPath.replaceAll("/([a-zA-Z_0-9\\-]+)", "/d:$1");

	try{
	    XPath xpath = xPathFactory.newXPath();
	    if(contextNode != null){
		NamespaceContextImpl nsContext = new NamespaceContextImpl(contextNode, "d");
		nsContext.setPrefix(MobyPrefixResolver.MOBY_XML_NAMESPACE, "d");
		xpath.setNamespaceContext(nsContext);
	    }
            peerElements = (NodeList) xpath.evaluate(peerGroupXPath, 
						     doc, 
						     XPathConstants.NODESET);
        } catch(Exception e){
            logger.log(Level.SEVERE, "Could not evaluate UserData XPath "+peerGroupXPath, e);
	    return null;
        }
        //System.err.println("Got " + peerElements.getLength() + " peers for " + peerGroupXPath);

	// Remove peers that are currently filtered.  Seems to be quicker than cloning the doc, 
	// filtering the node, then running the xpath.  Assumes you don't do much with peerElements
	// afterwards, or you should apply the filter to remove errant children, etc.)
	if(xPtrsToFilter != null && !xPtrsToFilter.isEmpty()){
	    MutableNodeList filterPassedPeers = new MutableNodeList();
	    for(int i = 0; i < peerElements.getLength(); i++){
		boolean filtered = false;
		String peerXPtr = getXPtr(peerElements.item(i));
		// Check all ancestors to see if they are filtered
		for(String ancestorXPtr = peerXPtr; 
		    ancestorXPtr.length() > 0;
		    ancestorXPtr = ancestorXPtr.replaceFirst("/\\d+$", "")){		
		    if(xPtrsToFilter.containsKey(ancestorXPtr)){
			filtered = true;
			break;
		    }
		}
		if(!filtered){
		    Element peerElement = (Element) peerElements.item(i);
		    // Now check if any child nodes need to be filtered
		    Map<String,String> childrenToDelete = new HashMap<String,String>();
		    for(String xptr: xPtrsToFilter.keySet()){
			// is it a child of the peer about to be put in the "passed filter" list? 
			if(xptr.indexOf(peerXPtr+"/") == 0){
			    childrenToDelete.put(xptr.substring(peerXPtr.length()), "whatever");
			}
		    }
		    if(!childrenToDelete.isEmpty()){
			peerElement = (Element) peerElements.item(i).cloneNode(true); //true = deep copy
			filterNodes(peerElement, childrenToDelete);
		    }
		    filterPassedPeers.add(peerElement);
		}
	    }
	    peerElements = filterPassedPeers;
	    //System.err.println("Got " + peerElements.getLength() + " peers that passed the filter");
	}

        return peerElements;
    }

    // given a node from a Moby XML dom, find the name of the mobyData job that's its parent
    public static String findMobyJobName(Node n){
        while(n != null && !n.getLocalName().equals(MobyTags.MOBYDATA)){
            n = n.getParentNode();
        }
        if(n == null){
            return null; // no mobyData parent
        }
        else{
            return MobyPrefixResolver.getAttr((Element) n, MobyTags.QUERYID);
        }
    }
}
