package ca.ucalgary.services;

import ca.ucalgary.services.util.IOUtils;
import ca.ucalgary.services.util.SourceMap;
import ca.ucalgary.services.util.WSDLConfig;
import ca.ucalgary.services.util.XHTMLForm;

import ca.ucalgary.seahawk.services.MobyClient;  // text to MOBY
import ca.ucalgary.seahawk.services.TextClient;  // MOBY to text

import org.biomoby.service.MobyServlet;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*; 

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import javax.xml.xpath.*;
//import javax.xml.ws.*;
// import org.apache.axis.AxisFault;
// import org.apache.axis.client.Call;
// import org.apache.axis.client.Service;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
import java.util.logging.*;

/**
 * A servlet to wrap a Semantically Annotated WSDL file into
 * a Moby Service using LSIDs to identify the models (Moby ontology terms) 
 * and lowering/lifting schema mappings (MOB and DEM rules) to bridge the 
 * semantic contents of Moby messages &lt;-&gt; the syntactic WSDL I/O.
 *
 * For more details, see <a href="http://biomoby.open-bio.org/CVS_CONTENT/moby-live/Java/docs/wrappingWSDL.html">the tutorial</a> 
 * on how to markup a WSDL file for Moby.
 * 
 * For more information on SAWSDL, see http://www.w3.org/TR/sawsdl/
 */
public class WSDLService extends WrapperService<WSDLConfig>{

    public static final String WSDL_URL_PARAM = "wsdlURL";
    protected URL remoteWSDLUrl;
    protected WSDLConfig wsdlConfig;
    //protected Dispatch<Source> dispatch;
    //PG protected Service webservice;
    protected URL endpoint;

    private Transformer nullTransformer;
    private Map<String,Transformer> secondaryTransformers; // secondary param to xml schema engines
    // SAWSDL deals with transformation URIs, but the transformation engine needs a data format name
    private Map<String,String> uri2XmlFormat; 
    private Map<String,XPathExpression> outXPathString2XPath; // precompile the output XPaths 
    private Map<String,Boolean> outXPathString2IsRegex; // is the rule to apply on the results of XPath a rehgex or another Xpath?

    private static LSIDResolver lsidResolver; // used to fetch transformation rules, etc. given in the SAWSDL attributes
    private static DocumentBuilder docBuilder;
    private static XPath xPath;  // essentially an XPathExpression builder

    // by default, expect the SourceMap to be populated with raw XML, not 
    // named values that are wrapped automatically
    private boolean usingRawXml = true; 

    private static Logger logger = Logger.getLogger(WSDLService.class.getName());
    private static final String DEFAULT_NSPREFIX_4_XPATH = "t";

    /**
     * C-tor for when you want to configure not from a SAWSDL file, but a configuration object.
     *
     * @param wsdlConfig the fully configured service def
     * @param usingRawXml will the lowering schema mappings generate the XML (true, default), or just the named text values for auto-XML-wrapping by SourceMap (false)
     */
    public WSDLService(WSDLConfig wsdlConfig, boolean usingRawXml){
	this();
	this.wsdlConfig = wsdlConfig;
	this.usingRawXml = usingRawXml;
    }

    public WSDLService(){
	super();
    }

    public void init(){
	super.init();

	// Explicitly set these so Java Web services will work in Java 1.5
	if(System.getProperty("javax.xml.stream.XMLInputFactory") == null){
	    System.setProperty("javax.xml.stream.XMLInputFactory",
			       "com.ctc.wstx.stax.WstxInputFactory");
	}

	if(System.getProperty("javax.xml.stream.XMLOutputFactory") == null){
	    System.setProperty("javax.xml.stream.XMLOutputFactory",
			       "com.ctc.wstx.stax.WstxOutputFactory");
	}

	try{
	    if(lsidResolver == null){
		lsidResolver = new LSIDResolver();
	    }
	} catch(Exception e){
	    logger.log(Level.SEVERE,
		       "Could not create an LSID Resolver: " + e.getMessage(),
		       e);
	}
	try{
	    if(xPath == null){
		//PG		xPath = XPathFactory.newInstance().newXPath();
		// For the moment, need to explicitly xalan use due to Google App Engine problem
		xPath = (new org.apache.xpath.jaxp.XPathFactoryImpl()).newXPath();
	    }
	} catch(Exception e){
	    logger.log(Level.SEVERE,
		       "Could not create an XPath: " + e.getMessage(),
		       e);
	}
	TransformerFactory transformerFactory = TransformerFactory.newInstance();
	try{
	    nullTransformer = transformerFactory.newTransformer();  // for verbatim copying of SOAP response XML
	} catch (TransformerConfigurationException tce){
	    logger.log(Level.SEVERE,
		       "Could not create an XSLT transformer: " + tce,
		       tce);
	}
	// Keep track of the sawsdl schema mapping uris and link them to the data format names
	// our engine needs (the mode attribute value in the XSLT template)
	uri2XmlFormat = new HashMap<String,String>();
	secondaryTransformers = new HashMap<String,Transformer>();
	outXPathString2XPath = new HashMap<String,XPathExpression>();
	outXPathString2IsRegex = new HashMap<String,Boolean>();
    }

    private static DocumentBuilder getDocBuilder(){
	if(docBuilder == null){
	    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	    dbf.setNamespaceAware(true);
	    try{
		docBuilder = dbf.newDocumentBuilder();
	    } catch(Exception e){
		e.printStackTrace();
	    }
	}
	return docBuilder;
    }

    public void processRequest(MobyDataJob request, MobyDataJob result) throws Exception{
	MobyService service = getService();

	// converts a map of strings into a source for the SOAP library
	QName qName = "rpc".equals(wsdlConfig.getOperationStyle()) ? 
	    new QName(wsdlConfig.getOperationInputQName().getNamespaceURI(), wsdlConfig.getOperationName()) : 
	    wsdlConfig.getOperationInputQName();
	SourceMap source = new SourceMap(qName, usingRawXml ? "raw" : wsdlConfig.getOperationEncoding());
	// we assume the results of the XSLTs used below contain the encoding tags as required.
	// this is solely for SAWSDL reasonableness...a better way would be to use
	// SourceMap to do the encoding, and just give it values from encoding-independent rules.  Oh well.
	//SourceMap source = new SourceMap(qName, sourceMode);

	// This is the mapping of moby parameter names to WS request & response 
	// xpaths for the transformed parameters
	Map<String,String> paramMap = wsdlConfig.getMobyParams2ServiceParams();

	Map<String,String> ruleURIs = wsdlConfig.getPrimaryInputFormats();
	for(MobyPrimaryData mobyInputTemplate: service.getPrimaryInputs()){
	    String paramName = mobyInputTemplate.getName();
	    MobyDataInstance mobyData = request.get(paramName);
	    if(!(mobyData instanceof MobyDataObject) &&
	       !(mobyData instanceof MobyDataObjectSet)){
		throw new MobyException("The Moby parameter '" + paramName + 
					"' is not a primary input as expected (" +
					"found " + mobyData.getClass().getName() + ")");
	    }
	    
	    // Transform the moby data as required and put it into the SOAP message
	    // NOTE: We assume, since it's WSDL, that the "legacy" format is XML Schema, which means the
	    // coercion of the legacy byte data to a String is kosher.
	    String xmlFormat = uri2XmlFormat.get(ruleURIs.get(paramName));

	    byte[] paramBytes = getLegacyData(mobyData, xmlFormat);
	    if(paramBytes == null){
		mobyData.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
		throw new MobyException("The Moby parameter '" + paramName + 
					"' could not be transformed into '" + 
					xmlFormat + 
					"' format by an XSLT rule.  Source data was:\n"+
					mobyData.toXML());
	    }
	    //System.err.println("In Param "+paramName+": "+
	    //	       new String(paramBytes));
	    source.put(mobyParam2SourceKey(paramMap, paramName),
		       new String(paramBytes));
	}

	// Now deal with the secondaries
	for(MobyDataSecondaryInstance mobySec: request.getSecondaryData()){
	    Transformer transformer = secondaryTransformers.get(mobySec.getName());
	    //System.err.println("Transformer class being used is " + transformer.getClass().getName());
	    mobySec.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
	    StreamSource secSrc = new StreamSource(new ByteArrayInputStream(mobySec.toXML().getBytes()));
	    StringWriter xsltResult = new StringWriter();
	    transformer.transform(secSrc, new StreamResult(xsltResult));
	    //System.err.println("Sec Param "+mobySec.getName()+": "+
	    //		       xsltResult.toString());
	    source.put(mobyParam2SourceKey(paramMap, mobySec.getName()), xsltResult.toString());
	}
	//System.err.println("Full source: " + source.toString());

	for(Map.Entry<String,String> fixedParam: wsdlConfig.getFixedParams().entrySet()){
	    source.put(fixedParam.getKey(), fixedParam.getValue());
	}

	//Source resultSource = dispatch.invoke(source);
//PG 	Call dispatch = (Call) webservice.createCall();
//PG 	dispatch.setTargetEndpointAddress(getSoapEndpoint(wsdlConfig.getPortQName()));
//PG	dispatch.setPortName(wsdlConfig.getPortQName());
 	String soapAction = wsdlConfig.getSoapAction();
//PG  	if(soapAction != null && soapAction.length() != 0){
//PG 	    dispatch.setSOAPActionURI(soapAction);
//PG 	}
//PG 	org.apache.axis.message.SOAPEnvelope payload = new org.apache.axis.message.SOAPEnvelope();
//PG 	synchronized(docBuilder){
//PG 	    payload.getBody().addDocument(docBuilder.parse(new ByteArrayInputStream(source.toString().getBytes())));
//PG 	}

//PG	Node resultSource = dispatch.invoke(payload).getBody().getFirstChild();

	//PG Temp hack since Google App Engine doesn't support JAX-WS
	URL u = getSoapEndpoint(wsdlConfig.getPortQName());
	HttpURLConnection conn = (HttpURLConnection) u.openConnection();
	IOUtils.writeToConnection(conn, source.toString().getBytes(), soapAction);
	Node resultSource = IOUtils.readFromConnection(conn);

	// for RPC responses handle the one-level data specially, so array are autoiterated
	// we don't use paramMap because you can't rename params in SAWSDL
	if("rpc".equals(wsdlConfig.getOperationStyle())){
	    Map<String,Node> responseData = getResponseData(resultSource);
	    
	    // Parse the results, matching up the correct parameter in the WSDL with the correct Moby type.
	    // Note that output WSDL and Moby names are the same, allowing auto-matchup.
	    // Also note that for the moment you can't write a MOB rule that aggregates multiple outputs.
	    for(MobyPrimaryData mobyOutputTemplate: service.getPrimaryOutputs()){
		if(!responseData.containsKey(mobyOutputTemplate.getName())){
		    System.err.println("Couldn't find required service output called " + mobyOutputTemplate.getName());
		    continue;  // Not an output Moby will use
		}
		MobyDataInstance mdi = getMobyData(responseData.get(mobyOutputTemplate.getName()), mobyOutputTemplate);
		if(mdi == null){
		    throw new Exception("The output parameter '" + mobyOutputTemplate.getName() + 
					"' of data type '" + mobyOutputTemplate.getDataType().getName() + 
					"' could not be created from the form submission response (" +
					"MobyClient returned null transforming the legacy data).");
		}
		result.put(mobyOutputTemplate.getName(), mdi);
	    }
	}
	else{
	    for(MobyPrimaryData mobyOutputTemplate: service.getPrimaryOutputs()){
		String resultXPath = paramMap.get(mobyOutputTemplate.getName());
		NodeList resultNodes = null;
		XPathExpression xPathExp = outXPathString2XPath.get(resultXPath);
		if(xPathExp == null){
		    throw new Exception("Could not find precompiled XPathExpression " +
					" to evaluate the output XPath (" + resultXPath +
					"): init() did not intialize properly");
		}
		try{
		    resultNodes = (NodeList) xPathExp.evaluate(resultSource, 
							       XPathConstants.NODESET);
		} catch(Exception e){
		    throw new Exception("Could not evaluate the XPath (" + resultXPath + 
					") in Node list mode to retrieve Moby output '"+ 
					mobyOutputTemplate.getName() +
					"' from the service response: " + e.getMessage(), e);
		}
		if(resultNodes.getLength() == 0){
		    logger.log(Level.WARNING, "Did not find any results for the XPath "+
			       resultXPath + " using node " + resultSource.getNodeName());
		}
		for(int i = 0; i < resultNodes.getLength(); i++){
		    Node resultNode = resultNodes.item(i);
		    // we don't expect every node to contain the value, so put a catch here
		    MobyDataInstance mdi = null;
		    try{
			// is the rule on the XML or the text inside the XML (XPath or regex rules should apply?)

			if(isRegexRule(resultXPath)){  // send text contents for regex rule eval
			    mdi = getMobyData(resultNode.getNodeValue(),
					      mobyOutputTemplate);
			}
			else{  // send Node for XPath rule eval
			    mdi = getMobyData(resultNode,
					      mobyOutputTemplate);
			}
		    } catch(Exception e){
			logger.log(Level.SEVERE, "Could not run MobyClient converters: " + e.getMessage(), e);
			continue;
		    }
		    if(mdi != null){
			result.put(mobyOutputTemplate.getName(), mdi);
		    }
		}
		//todo: make exception here optional?  not all services will return something useful all the time...
		if(result.isEmpty()){
		    throw new Exception("The output parameter '" + mobyOutputTemplate.getName() + 
					"' of data type '" + mobyOutputTemplate.getDataType().getName() + 
					"' could not be created from the form submission response (" +
					"MobyClient returned null transforming the legacy data).");
		}
	    }
	}
    }
   
    private boolean isRegexRule(String xpath){
	return outXPathString2IsRegex.get(xpath).booleanValue();
    }

    private String mobyParam2SourceKey(Map<String,String> mobyParamName2wsdlXPathMap,
				       String mobyParamName){
	if(mobyParamName2wsdlXPathMap == null){
	    return mobyParamName; //no mapping available, use as-is (mostly for RPC-style SAWSDL)
	}
	String xpath = mobyParamName2wsdlXPathMap.get(mobyParamName);
	return xpath.replaceAll("/", ":");  //SourceMap uses colon instead of slash to denote tag nesting
    }

    //  parse the wsdl to getthe URL where the call should be made
    private URL getSoapEndpoint(QName targetPort) throws Exception{
	if(targetPort == null || targetPort.getLocalPart() == null){
	    throw new Exception("Asked for SOAP endpoint with a null port name");
	}
	if(endpoint != null){
	    return endpoint;
	}
	if(wsdlConfig != null){
	    Document wsdlDoc = getDocBuilder().parse(wsdlConfig.getSpecURL().openStream());
	    NodeList ports = wsdlDoc.getDocumentElement().getElementsByTagNameNS("http://schemas.xmlsoap.org/wsdl/", "port");
	    for(int i = 0; i < ports.getLength(); i++){
		Element portElem = (Element) ports.item(i);
		String portName = portElem.getAttribute("name");
		if(targetPort.getLocalPart().equals(portName)){
		    NodeList addrs = portElem.getElementsByTagNameNS("http://schemas.xmlsoap.org/wsdl/soap/", "address");
		    for(int j = 0; j < addrs.getLength(); j++){
			Element addrElem = (Element) addrs.item(j);
			String location = addrElem.getAttribute("location");
			if(location != null && location.length() != 0){
			    endpoint = new URL(location);
			    logger.log(Level.SEVERE,
				       "####The SOAP endpoint is " + endpoint);
			    return endpoint;
			}
		    }
		}
	    }
	}
	return null;
    }


    public MobyService createServiceFromConfig(javax.servlet.http.HttpServletRequest request)
        throws Exception{

	// we were given the WSDLConfig explicitly in the c-tor to override the default resolution
	if(wsdlConfig == null){
	    remoteWSDLUrl = getSpecURL(WSDL_URL_PARAM);
	    try{
		wsdlConfig = new WSDLConfig(remoteWSDLUrl);
	    } catch(Exception e){
		logger.log(Level.SEVERE,
			   "Could not determine Moby service configuration from the WSDL (" +
			   remoteWSDLUrl.toString() + ")",
			   e);
		throw e;
	    }
	}
        // Call to parent, which handles spec-wrapper-to-MobyServlet-config conversion
	// Need to call the parent so that addLoweringMappingsFromURL(), etc. calls below
	// work on the instantiated WrapperService data transformation engines (TextClient and MobyClient)
	// FYI: config contains all of the info about how many parameters there are, their types, etc.
	MobyService ms = createServiceFromConfig(request, wsdlConfig);

	createServiceFromConfig();
	return ms;
    }

    public void createServiceFromConfig() throws Exception{

	// Verify if the service info was actually parsed properly by trying to use it with JAX-WS
// 	Service service = null;
//PG 	try{
// 	    service = Service.create(wsdlConfig.getSpecURL(), 
// 				     wsdlConfig.getServiceQName());
//PG	    webservice = new Service(//wsdlConfig.getSpecURL(),
//PG				     wsdlConfig.getServiceQName());
//PG 	} catch(Exception e){
//PG 	    logger.log(Level.SEVERE,
//PG 		       e.getClass().getName() + " while using JAX-WS to create a handle for " +
//PG 		       "the service, either the WSDL or the WSDLConfig's serviceQName parsed (" +
//PG 		       wsdlConfig.getServiceQName() + ") is wrong",
//PG 		       e);
//PG 	    throw e;
//PG 	}

// 	try{
// 	    dispatch = service.createDispatch(wsdlConfig.getPortQName(),
// 					      Source.class,
// 					      Service.Mode.PAYLOAD);
// 	} catch(Exception e){
// 	    logger.log(Level.SEVERE,
// 		       e.getClass().getName() + " while using JAX-WS to create a dispatch for a port on " +
// 		       "the service " + wsdlConfig.getServiceQName() + ", either the WSDL or the WSDLConfig's " +
// 		       "portQName parsed (" + wsdlConfig.getPortQName() + ") is wrong",
// 		       e);
// 	    throw e;
// 	}

	// Some servers need the soap action set to know what method to invoke
// 	String soapAction = wsdlConfig.getSoapAction();
// 	if(soapAction != null && soapAction.length() != 0){
// 	    Map<String,Object> context = dispatch.getRequestContext();	
// 	    context.put(Dispatch.SOAPACTION_USE_PROPERTY, Boolean.TRUE);
// 	    context.put(Dispatch.SOAPACTION_URI_PROPERTY, soapAction);
// 	}

	// Resolve the LSIDs used to describe the I/O formats into their actual data 
	// (Moby DEM and MOB conversion rules)
	for(String schemaMappingLSID: wsdlConfig.getPrimaryInputFormats().values()){
	    URL u = null;
	    try{
		u = lsidResolver.resolveDataURL(schemaMappingLSID);		
	    } catch(Exception e){
		logger.log(Level.SEVERE,
			   e.getClass().getName() + " while resolving lowering schema mapping LSID " +
			   schemaMappingLSID +" from the SAWSDL document",
			   e);
		throw e;
	    }
	    try{
		String dataFormatReturned = addLoweringMappingsFromURL(u);
		uri2XmlFormat.put(schemaMappingLSID, dataFormatReturned);
	    } catch(Exception e){
		logger.log(Level.SEVERE,
			   e.getClass().getName() + " while parsing lowering schema mapping (LSID " +
			   schemaMappingLSID +")",
			   e);
		throw e;
	    }
	}
	
	Map<String,String> paramMap = wsdlConfig.getMobyParams2ServiceParams();
	Map<String,String> outParamURIs = wsdlConfig.getPrimaryOutputFormats();
	// note, that because of the context, the xpath will probably fail if an xpath function is called
	NamespaceContextImpl targetNSContext = null;
	if(wsdlConfig.getTargetNamespaceURI() == null){
	    targetNSContext = new NamespaceContextImpl();
	    logger.log(Level.WARNING, 
		       "***NO namespace for XPaths");
	}
	else{
	    targetNSContext = new NamespaceContextImpl(wsdlConfig.getTargetNamespaceURI(), DEFAULT_NSPREFIX_4_XPATH);
	    logger.log(Level.WARNING, 
		       "***Set target namespace (prefix " + DEFAULT_NSPREFIX_4_XPATH + 
		       ") for XPaths to "+wsdlConfig.getTargetNamespaceURI());
	}
	for(String paramName: outParamURIs.keySet()){
	    String schemaMappingLSID = outParamURIs.get(paramName);
	    URL u = null;
	    try{
		u = lsidResolver.resolveDataURL(schemaMappingLSID);		
	    } catch(Exception e){
		logger.log(Level.SEVERE,
			   e.getClass().getName() + " while resolving lifting schema mapping LSID " +
			   schemaMappingLSID +" from the SAWSDL document",
			   e);
		throw e;
	    }
	    try{
		addLiftingMappingsFromURL(u);
	    } catch(Exception e){
		logger.log(Level.SEVERE,
			   e.getClass().getName() + " while parsing lifting schema mapping (LSID " +
			   schemaMappingLSID +")",
			   e);
		throw e;
	    }
	    // Record if the rule is a regex or if it is xpath-based for later usage...
	    outXPathString2IsRegex.put(paramName, getMobyClient().getPattern(schemaMappingLSID) != null);

	    // Haha! the name of the parameter *is* the xpath.  How clever.
	    // with the exception of the fact an attribute and the text of an element
	    // may both be useful, so we kept the /@... or /text() suffixes
	    // to avoid name collision in the parameter hash. To get the result node
	    // on which MOB rules will be applied, we need to strip these parts off
	    // don't worry, they appear in the MOB rules (unless its a regex), so we get the correct response.
	    // Here we're just getting the evaluation context for the MOB rule.
	    String resultXPathString = paramName;
	    if(!outXPathString2IsRegex.get(paramName)){
		resultXPathString = resultXPathString.replaceAll("/(?:text\\(\\)|@[^/]+)$","");
	    }
	    // We will cheat slightly by making any paths e.g. /a/b/c into //a/b/c 
	    // because the XML doc may actually contain the soap envelope, etc.
	    if(resultXPathString.indexOf("//") != 0){
		resultXPathString = fixXPath(resultXPathString).substring(1); 
		resultXPathString = resultXPathString.substring(resultXPathString.indexOf("/")+1);
	    }
	    try{
		//temp: we use IOUtils, which gives root element, not document node.  Lop off first level of xpath
		resultXPathString = resultXPathString.replaceFirst("^/[^/]+/","");
		xPath.setNamespaceContext(targetNSContext);
		outXPathString2XPath.put(paramName,
					 xPath.compile(resultXPathString));
		logger.log(Level.WARNING, "%%%%%%Compiled " + resultXPathString);
	    } catch(Exception e){
		logger.log(Level.SEVERE,
			   "While parsing service result XPath (" + resultXPathString +
			   "): " + e.getMessage(),
			   e);
		throw e;
	    }
	}

	// Deal with secondary parameters
	TransformerFactory transformerFactory = TransformerFactory.newInstance();
	for(Map.Entry<String,String> mapping: wsdlConfig.getSecondaryInputFormats().entrySet()){
	    URL u = null;
	    try{
		u = lsidResolver.resolveDataURL(mapping.getValue());		
	    } catch(Exception e){
		logger.log(Level.SEVERE,
			   e.getClass().getName() + " while resolving lifting schema mapping LSID " +
			   mapping.getValue() +" from the SAWSDL document for secondary parameter " +
			   mapping.getKey(),
			   e);
		throw e;
	    }
	    Transformer transformer = null;
	    try{
		transformer = transformerFactory.newTransformer(new StreamSource(u.openStream()));
	    } catch (TransformerConfigurationException tce){
		logger.log(Level.SEVERE,
			   "Could not create an XSLT transformer: " + tce,
			   tce);
		throw tce;
	    }
	    secondaryTransformers.put(mapping.getKey(), transformer);
 	}
    }

     private String fixXPath(String xpath){
	 //return xpath.replaceAll("/([a-zA-Z_0-9\\-]+)(?=/|$|\\[)", "/"+DEFAULT_NSPREFIX_4_XPATH+":$1");
	return xpath.replaceAll("/([a-zA-Z_0-9\\-]+)(?=/|$)", "/*[local-name() = '$1']")
	    .replaceAll("/([a-zA-Z_0-9\\-]+)\\[", "/*[local-name() = '$1' and ");
     }

    public String createInputSpecString(WSDLConfig wsdl){
	Map<String,String> params = wsdlConfig.getPrimaryInputs();
	//System.err.println("Inputs: "+XHTMLForm.join(",", params.values().toArray(new String[params.size()])));
	return XHTMLForm.join(",", params.values().toArray(new String[params.size()]));
    }

    public String createOutputSpecString(WSDLConfig form){
	Map<String,String> params = wsdlConfig.getPrimaryOutputs();
	//System.err.println("Outputs: "+XHTMLForm.join(",", params.values().toArray(new String[params.size()])));
	return XHTMLForm.join(",", params.values().toArray(new String[params.size()]));
    }

    public String createSecondarySpecString(WSDLConfig form){
	Map<String,String> params = wsdlConfig.getSecondaryInputs();
	//System.err.println("Secondaries: "+XHTMLForm.join(",", params.values().toArray(new String[params.size()])));
	return XHTMLForm.join(",", params.values().toArray(new String[params.size()]));
    }

    private Map<String,Node> getResponseData(Node domNode) throws Exception{
    //    private Map<String,Node> getResponseData(Source source) throws Exception{
	// Structure is (SOAP output param name -> XML Schema Instance data)
	 Map<String,Node> data = new HashMap<String,Node>();
// 	DOMResult domResult = new DOMResult();
// 	nullTransformer.transform(source, domResult);

// 	Node domNode = domResult.getNode();
// 	if(domNode == null){
// 	    throw new Exception("Could not get the result node from the DOM (bad null transformation?)");
// 	}
 	Document owner = (domNode instanceof Document ? (Document) domNode : domNode.getOwnerDocument());
// 	if(owner == null){
// 	    throw new Exception("Could not get the owner document from the DOM result (owner was null, node was " + 
// 				domNode.getClass().getName()+")");
// 	}
// 	Element responseElement = owner.getDocumentElement();
	 Element responseElement = (Element) domNode;

	NodeList responseParams = responseElement.getChildNodes();
	for(int i = 0; i < responseParams.getLength(); i++){	    
	    if(!(responseParams.item(i) instanceof Element)){
		continue;
	    }
	    Element responseParam = (Element) responseParams.item(i);
	    
	    // Use a TreeMap (as opposed to HashMap) so that the order is preserved on iteration
	    TreeMap<String,byte[]> responseValues = new TreeMap<String,byte[]>();
	    
	    // If the returned param is an array (xsi:type="SOAP-ENC:Array"), iterate over the values
	    String soapType = responseParam.getAttributeNS(MobyPrefixResolver.XSI_NAMESPACE2001, "type");
	    if(soapType != null && soapType.endsWith(":Array")){
		data.put(responseParam.getNodeName(), 
			 WSDLConfig.renameSoapArrayElements(responseParam, responseParam.getNodeName(), owner));
	    }
	    else{
		data.put(responseParam.getNodeName(), responseParam);
	    }

// 	    StringWriter stringWriter = new StringWriter();
// 	    nullTransformer.transform(new DOMSource(data.get(responseParam.getNodeName())), new StreamResult(stringWriter));
// 	    System.err.println(responseParam.getNodeName()+": "+ stringWriter.toString());
	}

	return data;
    }

}
