package ca.ucalgary.services.util;

import ca.ucalgary.services.SoapServlet;

import org.biomoby.shared.LSIDResolver;
import org.biomoby.shared.MobyPrefixResolver;
import org.biomoby.shared.NamespaceContextImpl;
import org.biomoby.shared.parser.MobyTags;

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.*;
import javax.xml.xpath.*;
import javax.xml.ws.*;

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

/**
 * Used to expose a WSDL file enhanced with SAWSDL and Moby markup as a Moby service.
 */
public class WSDLConfig extends MobySpecWrapper{

    public static final String SERVICE_NAME_ATTR = "serviceName";
    public static final String SERVICE_DESC_ATTR = "serviceDesc";
    public static final String SERVICE_AUTH_ATTR = "serviceAuthority";
    public static final String SERVICE_CONTACT_ATTR = "serviceContact";
    public static final String REGISTRY_ATTR = "registryEndpoint";
    public static final String DEFAULT_AUTHORITY = "biomoby.org";

    public static final String SAWSDL_MODEL_ATTR = "modelReference";
    public static final String SAWSDL_INMAP_ATTR = "loweringSchemaMapping";
    public static final String SAWSDL_OUTMAP_ATTR = "liftingSchemaMapping";
    public static final String MOBY_SECONDARY_SOURCE_ATTR = "secondaryParamSource";

    private final String ARRAY_SENTINEL = "applyToEachXSIArrayItem";
    private final String SERVICENAME_ATTR_XPATH = "//sawsdl:attrExtensions"; 
    // private final String SERVICENAME_ATTR_XPATH = "//*[@moby:"+SERVICE_NAME_ATTR+"]"; 
    // note that the XPath above doesn't work, not sure why at the moment, so we don't support WSDL2.0 yet...

    private static XPath xPath;
    private static DocumentBuilder docBuilder;
    private Document wsdlDoc;
    private URL wsdlURL;

    private Map<String,QName> mobyServiceName2Service;
    private Map<String,QName> mobyServiceName2Port;
    private Map<String,QName> mobyServiceName2OpInput;
    private Map<String,QName> mobyServiceName2OpOutput;
    private Map<String,String> mobyServiceName2Op;
    private Map<String,String> mobyServiceName2SoapAction;
    private Map<String,String> mobyServiceName2TargetNamespaceURI;
    private Map<String,String> mobyServiceName2Style;    //document or rpc
    private Map<String,String> mobyServiceName2Encoding; //literal or encoded
    private Map<String,Map<String,String>> mobyServiceName2InputXSDTypes;
    private Map<String,Map<String,String>> mobyServiceName2OutputXSDTypes;
    private LSIDResolver lsidResolver;

    private static Logger logger = Logger.getLogger("ca.ucalgary.services.util.WSDLConfig");

    static{
        // Commented out generic factory due to Google App Engine bug
	// XPathFactory xPathFactory = XPathFactory.newInstance();
	XPathFactory xPathFactory = new org.apache.xpath.jaxp.XPathFactoryImpl();
	try{
	    xPath = xPathFactory.newXPath();
	    xPath.setNamespaceContext(new NamespaceContextImpl());
	} catch(Exception e){
            e.printStackTrace();
        } 

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

    /**
     * C-tor to use when you'll programmatically set the values rarther than getting them from a SAWSDL file.
     */
    public WSDLConfig(){
	mobyServiceName2Service = new HashMap<String,QName>();
	mobyServiceName2Port = new HashMap<String,QName>();
	mobyServiceName2SoapAction = new HashMap<String,String>();
	mobyServiceName2Style = new HashMap<String,String>();     //document or rpc
	mobyServiceName2Encoding = new HashMap<String,String>();  //literal or encoded
	mobyServiceName2Op = new HashMap<String,String>();
	mobyServiceName2OpInput = new HashMap<String,QName>();
	mobyServiceName2OpOutput = new HashMap<String,QName>();
	mobyServiceName2InputXSDTypes = new HashMap<String,Map<String,String>>();
	mobyServiceName2OutputXSDTypes = new HashMap<String,Map<String,String>>();
	mobyServiceName2TargetNamespaceURI = new HashMap<String,String>();

	lsidResolver = new LSIDResolver();	
    }

    public WSDLConfig(URL url) throws Exception{
	this();
	wsdlURL = url;

	setSpecURL(wsdlURL);
	parse(wsdlURL.openStream());
    }

    /**
     *  General <i>modus operandi</i>:
     * <ul>
     *   <li>Find the moby:serviceName attributes</li>
     *   <li>Trace back for each the operation name with which it is associated (and ensure other required moby: attrs are defined)</li>
     *   <li>Trace back the message definitions for the operation (and ensure SAWSDL schema lifting/lower attrs are okay)</li>
     *   <li>Trace back the service and port type with which the operation is associated</li>
     * </ul>
     *
     * This procedure lets us blissfully ignore any non-Moby SASWDL operations.
     */
    protected void parse(InputStream is) throws Exception{
	wsdlDoc = docBuilder.parse(is);
	// put wsdl imports in-place as pertinent parts of the definition 
	// of the service maybe spread over multiple files.
	SoapServlet.doImports(wsdlDoc, wsdlURL);

	// Find the moby:serviceName attributes
	parseMobyServiceSpecs();
	parseSOAPMessageSpecs();
    }

    // Finds out the semantic mapping data for the I/O of each operation being wrapped as a Moby service
    public void parseSOAPMessageSpecs() throws Exception{
	for(String serviceName: getServiceNames()){
	    setCurrentService(serviceName);
	    String currentInputMessage = getOperationInputQName().getLocalPart();
	    String currentOutputMessage = getOperationOutputQName().getLocalPart();

	    NodeList messageElements = wsdlDoc.getDocumentElement().getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE, 
											   "message");
	    for(int i = 0; i < messageElements.getLength(); i++){
		String messageName = ((Element) messageElements.item(i)).getAttribute("name");
		if(currentInputMessage.equals(messageName)){
		    parseSOAPMessageSpec((Element) messageElements.item(i), messageName, true);
		}
		else if(currentOutputMessage.equals(messageName)){
		    parseSOAPMessageSpec((Element) messageElements.item(i), messageName, false);
		}
	    }
	}
    }

    private void parseSOAPMessageSpec(Element messageElement, String messageName, boolean isInputMessage) throws Exception{
	NodeList partElements = messageElement.getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE, "part");
	Map<String,String> inputs = new HashMap<String,String>();
	Map<String,String> inputMappings = new HashMap<String,String>();
	Map<String,String> secondaryMappings = new HashMap<String,String>();
	Map<String,String> inputTypes = new HashMap<String,String>();
	Map<String,String> secondaryInputs = new HashMap<String,String>();
	Map<String,String> outputs = new HashMap<String,String>();
	Map<String,String> outputMappings = new HashMap<String,String>();
	Map<String,String> outputTypes = new HashMap<String,String>();

	for(int i = 0; i < partElements.getLength(); i++){
	    Element partElement = (Element) partElements.item(i);
	    String partName = partElement.getAttribute("name");
	    if(partName == null || partName.length() == 0){
		throw new Exception("A part element has no name in the definition of the WSDL message named " + messageName);
	    }
	    String partType = partElement.getAttribute("type");
	    // Here RPC/Encoded and Document Literal start to differ, as Doc/Lit will refer the element definitions, 
	    // whereas RPC/Encoded will define the types right here.
	    if(partType == null || partType.length() == 0){
		String elementType = partElement.getAttribute("element");
		if(elementType == null || elementType.trim().length() == 0){
		    throw new Exception("Part element " + partName + " of WSDL message " + 
					messageName + " has no type (rpc/encoded) or element (doc/lit) attribute defined");
		}
		// A qualified name?
		if(elementType.indexOf(":") != -1){
		    elementType = elementType.substring(elementType.indexOf(":")+1);
		}
		// Find the definition, it should have the sawsdl stuff in it
		NodeList elementDefs = (NodeList) xPath.evaluate("//xsd:element[@name='"+elementType+"']",
								  wsdlDoc,
								  XPathConstants.NODESET);
		if(elementDefs == null || elementDefs.getLength() == 0){
		    throw new Exception("The definition of the element '" + elementType + 
					"' (for message " + messageName + ", part " + partName + 
					") could not be found in the WSDL file.");		    
		}		
		if(elementDefs.getLength() != 1){
		    throw new Exception("Found more than one definition for element " + 
					elementType + " in the WSDL file");		    
		}
		// Find the sub elements that make up the document element contents, each should have its one
		// schema mapping (we don't yet support 1:n moby:xmlschema mappings, or "deep" mappings of nested subelements).
		NodeList subElements = ((Element) elementDefs.item(0)).getElementsByTagName("element");
		for(int j = 0; j < subElements.getLength(); j++){
		    Element subElement = (Element) subElements.item(j);
		    String subPartName = subElement.getAttribute("name");
		    String subPartType = subElement.getAttribute("type");
		    if(subPartType == null || subPartType.trim().length() == 0){
			throw new Exception("No type attribute was found for " + elementType + " subelement " + subPartName);
		    }
		    addIOMapping(subElement, messageName, subPartType, subPartName,
				 inputs, inputMappings, secondaryMappings, inputTypes, 
				 secondaryInputs, outputs, outputMappings, outputTypes, isInputMessage);
		}
	    }
	    else{
		addIOMapping(partElement, messageName, partType, partName, 
			     inputs, inputMappings, secondaryMappings, inputTypes, 
			     secondaryInputs, outputs, outputMappings, outputTypes, isInputMessage);
	    }
	}
	if(isInputMessage){
	    setPrimaryInputs(inputs);
	    setPrimaryInputFormats(inputMappings);
	    setInputXSDTypes(inputTypes);
	    setSecondaryInputs(secondaryInputs);
	    setSecondaryInputFormats(secondaryMappings);
	}
	else{ // it's output
	    setPrimaryOutputs(outputs);
	    setPrimaryOutputFormats(outputMappings);
	    setOutputXSDTypes(outputTypes);	    
	}
    }

    private void addIOMapping(Element partElement, String messageName, String partType, String partName, 
			      Map<String,String> inputs,
			      Map<String,String> inputMappings,
			      Map<String,String> secondaryMappings, 
			      Map<String,String> inputTypes,
			      Map<String,String> secondaryInputs, 
			      Map<String,String> outputs, 
			      Map<String,String> outputMappings, 
			      Map<String,String> outputTypes,
			      boolean isInputMessage) throws Exception{
	String modelReference = partElement.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_MODEL_ATTR);
	if(modelReference == null || modelReference.length() == 0){
	    // TODO deal with schema-only level SAWSDL annotation
	    throw new Exception("Part element " + partName + " of WSDL message " + 
				messageName + " has no SAWSDL " + SAWSDL_MODEL_ATTR + " attribute defined");
	}
	String schemaMapping = null;
	if(isInputMessage){
	    schemaMapping = partElement.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_INMAP_ATTR);
	    if(schemaMapping == null || schemaMapping.length() == 0){
		throw new Exception("Part element " + partName + " of WSDL message " + 
				    messageName + " has no SAWSDL " + SAWSDL_INMAP_ATTR + " attribute defined");
	    }
	    String mobyParam = wsdlParam2MobyParam(partElement, partName, partType, modelReference);
	    if(mobyParam == null){
		// Check if it's a secondary
		mobyParam = wsdlParam2MobySecondaryParam(partElement, partName, partType, modelReference);
		if(mobyParam == null){
		    throw new Exception("The LSID '" + modelReference + 
					"' is not a properly formed Moby primary or secondary param LSID as required");
		}
		secondaryInputs.put(partName, mobyParam);
		secondaryMappings.put(partName, schemaMapping);
	    }
	    else{
		inputs.put(partName, mobyParam);
		inputTypes.put(partName, partType);
		inputMappings.put(partName, schemaMapping);
	    }
	}
	else{
	    schemaMapping = partElement.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_OUTMAP_ATTR);
	    if(schemaMapping == null || schemaMapping.length() == 0){
		throw new Exception("Part element " + partName + " of WSDL message " + 
				    messageName + " has no SAWSDL " + SAWSDL_OUTMAP_ATTR + " attribute defined");
	    }
	    outputs.put(partName, wsdlParam2MobyParam(partElement, partName, partType, modelReference));
	    outputTypes.put(partName, partType);
	    outputMappings.put(partName, schemaMapping);	    
	}
    }

    public String wsdlParam2MobyParam(Element element, String paramName, String paramXSDType, String dataTypeLSID) 
	throws Exception{

	// See if the name is namespace qualified, and separate the local part if so
	String nsURI = "";
	if(paramXSDType.contains(":")){
	    String nsPrefix = paramXSDType.substring(0, paramXSDType.indexOf(":"));  //XML NS prefix
	    paramXSDType = paramXSDType.substring(paramXSDType.indexOf(":")+1); //local part
	    nsURI = element.lookupNamespaceURI(nsPrefix);  //prefix->URI
	}
	Element schemaDataTypeElement = isBasicType(paramXSDType) ? null : 
	    getSchemaElement(element.getOwnerDocument(), nsURI, paramXSDType);	    
	boolean isArray = getArrayType(schemaDataTypeElement) != null;

	// The reference may include multiple values, separated by spaces
	String[] dataTypeLSIDs = dataTypeLSID.split(" ");
	String returnDataType = null;
        String returnNamespaces = null;

	for(String lsid: dataTypeLSIDs){
	    String[] dataTypeLSIDFields = lsid.split(":");
	    if(dataTypeLSIDFields.length != 5 && dataTypeLSIDFields.length != 6 ||
	       !dataTypeLSIDFields[0].equals("urn") || !dataTypeLSIDFields[1].equals("lsid") ||
	       (!dataTypeLSIDFields[3].equals("objectclass") && !dataTypeLSIDFields[3].equals("namespacetype"))){
		// silently ignore secondaries...we'll catch them later in the code
		if(!dataTypeLSIDFields[3].equals("secondaryParamClass")){
		    logger.log(Level.WARNING, "Ignoring '" + lsid + "' annotation as it does not appear to be a Moby LSID");
		}
		continue;
	    }
	    
	    if(dataTypeLSIDFields[3].equals("namespacetype")){
		returnNamespaces = returnNamespaces == null ? dataTypeLSIDFields[4] : 
		                                              returnNamespaces + "," + dataTypeLSIDFields[4];
	    }
	    else if(returnDataType == null){
		if(isArray){
		    // Deal with the case where the output XSD data type is an array: 
		    // implicitly make the return data type Collection(Thing)
		    returnDataType = "Collection("+dataTypeLSIDFields[4]+")";
		}
		else{
		    returnDataType = dataTypeLSIDFields[4];
		}
	    }
	    else{
		Exception e = new IllegalArgumentException("The annotation for " + paramName + 
							   " contains more than one datatype (" + returnDataType + " and " +
							   lsid + "), please remove one.  Multiple namespaces, but not " +
							   "multiple datatypes are allowed.");
		logger.log(Level.SEVERE, "While parsing annotation for " + paramName, e);
		throw e;
	    }
	}
	if(returnDataType == null){
	    if(returnNamespaces == null){
		return null; // No moby annotation found
	    }
	    returnDataType = isArray ? "Collection("+MobyTags.MOBYOBJECT+")" : MobyTags.MOBYOBJECT;
	}
	return paramName+":"+returnDataType+(returnNamespaces == null ? "" : ":"+returnNamespaces);
    }

    private boolean isBasicType(String xsdType){
	return "string".equals(xsdType); //TODO: fill out list
    }

    public String wsdlParam2MobySecondaryParam(Element partElement, String paramName, 
					       String paramXSDType, String dataTypeLSID) throws Exception{
	String[] dataTypeLSIDFields = dataTypeLSID.split(":");
	if(dataTypeLSIDFields.length != 5 && dataTypeLSIDFields.length != 6 ||
	   !dataTypeLSIDFields[0].equals("urn") || !dataTypeLSIDFields[1].equals("lsid") ||
	   !dataTypeLSIDFields[3].equals("secondaryParamClass")){
	    return null;
	}   
	// the format for secondaries is "name:type:defaultValue:[min,max]", where [min,max] can be an enumeration as well
	String restrictionsSpec = getSecondaryParamRestrictions(partElement);
	//System.err.println("Full secondary spec is " + paramName+":"+dataTypeLSIDFields[4]+":"+restrictionsSpec);
	return paramName+":"+dataTypeLSIDFields[4]+":"+restrictionsSpec;
    }

    /**
     * Checks out the Moby secondary parameter source attribute.  If it's a reference to another service, 
     * with a nullary c-tor, that service is executed and the returned values are used in the secondary param spec
     * valid value range.  The value could also be a literal pointed to by a Java-acceptable URL.
     * 
     * The returned value is of the form "defaultValue:[min,max]", where [min,max] can be an enumeration as well
     */
    public String getSecondaryParamRestrictions(Element partElement) throws Exception{
	String secondaryValueSource = partElement.getAttributeNS(MobyPrefixResolver.MOBY_XML_NAMESPACE, 
								 MOBY_SECONDARY_SOURCE_ATTR);
	try{
	    URL u = new URL(secondaryValueSource);
	    return getSecondaryParamRestrictionsFromURL(u);
	} catch(Exception e){
	    // TODO: handle LSID case?
	}
	// we get here if the source was not a URL or LSID, assume it's a service (WSDL operation)
	return getSecondaryParamRestrictionsFromService(partElement, secondaryValueSource);
    }

    public String getSecondaryParamRestrictionsFromURL(URL u){
	// TODO
	return "";
    }

    public String getSecondaryParamRestrictionsFromService(Element partElement, String opName) throws Exception{
	String defaultValue = "";

	Element wsdlRoot = partElement.getOwnerDocument().getDocumentElement();
	// Find the operation element defining the op specified
	Element opElement = null;
	NodeList ops = wsdlRoot.getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE, "operation");
	if(ops.getLength() == 0){
	    ops = wsdlRoot.getElementsByTagNameNS(MobyPrefixResolver.WSDL20_NAMESPACE, "operation");
	}
	for(int i = 0; i < ops.getLength(); i++){
	    Element opE = (Element) ops.item(i);
	    if(opName.equals(opE.getAttribute("name"))){
		opElement = opE;
		break;
	    }
	}
	if(opElement == null){
	    logger.log(Level.SEVERE,
		       "While trying to find a WSDL operation providing secondary parameter values, " +
		       "either the WSDL or the specified parameter source operation (" + opName + ") is wrong");
	    throw new IllegalArgumentException("While trying to find a WSDL operation providing secondary parameter values, " +
		       "either the WSDL or the specified parameter source operation (" + opName + ") is wrong");
	}

	QName[] specs = getServiceAndPortFromOperation(opElement, opName);  //backtracks from the op to the qnames JAX-WS needs

	Service service = null;
	try{
	    service = Service.create(wsdlURL, specs[0]);
	} catch(Exception e){
	    logger.log(Level.SEVERE,
		       e.getClass().getName() + " while using JAX-WS to create a handle for" +
		       "a service (" + specs[0] + ") providing secondary parameter values, either the WSDL or the " +
		       "specified parameter source operation (" + opName + ") is wrong",
		       e);
	    throw e;
	}

	Dispatch<Source> dispatch = null;
	try{
	    dispatch = service.createDispatch(specs[1],
					      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 " + specs[0] + ", either the WSDL or the WSDLConfig's " +
		       "portQName parsed (" + specs[1] + ") is wrong",
		       e);
	}
	Map<String,Object> context = dispatch.getRequestContext();
	context.put(Dispatch.SOAPACTION_USE_PROPERTY, Boolean.TRUE);
	context.put(Dispatch.SOAPACTION_URI_PROPERTY, specs[0].getNamespaceURI()+"#"+opName);
	
	URL liftingSchema = getLiftingSchemaFromOperation(opElement, opName);
	// Cheating slightly, since we can't return a URL and a boolean, the ref part of 
        // the URL is set if we want to indicate the rule should be applied to all members of an XSI array
	boolean isArrayRule = false;
	if(ARRAY_SENTINEL.equals(liftingSchema.getRef())){
	    isArrayRule = true;
	}

	// we expect the method to have a nullary signature, so we just create a blank input map
	SourceMap source = new SourceMap(new QName(specs[0].getNamespaceURI(), opName), specs[2].getLocalPart()); 

	Source resultSource = dispatch.invoke(source);
	// Parse the results into the datatype required
	//System.err.println("Class of response is " + resultSource.getClass().getName());
	
	String rangeValues = convertSourceToMobySecondaryValues(resultSource, liftingSchema, isArrayRule);
	//System.err.println("Range values are:\n"+rangeValues);

	if(rangeValues == null || rangeValues.length() == 0){
	    throw new Exception("No values for the Moby secondary paramater could " +
				"be found from nullary operation "+opName);
	}
	if(rangeValues.indexOf(',') == -1){
	    defaultValue = rangeValues;  // only one choice anyway
	}
	// Pick the first value as the default value, for lack of a better strategy
	else{
	    defaultValue = rangeValues.substring(0, rangeValues.indexOf(','));
	}
	return defaultValue + ":[" + rangeValues + "]";
    }

    /**
     * Goes from an operation, determines the output datatype (THERE SHOULD BE JUST ONE), 
     * then sees if that datatype has a lifting schema
     * that'll turn the response into a moby secondary param format.
     */
    protected URL getLiftingSchemaFromOperation(Element opElement, String opName) throws Exception{
	QName[] ioMsgNames = getInputAndOutputMessageNamesFromOperation(opElement);
	// ioMsgNames[1] is the output message name
	String targetMessageName = ioMsgNames[1].getLocalPart();

	// Now find the message definition in the WSDL
	Element wsdlRoot = opElement.getOwnerDocument().getDocumentElement();
	NodeList messageElements = wsdlRoot.getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE, 
								   "message");
	Element partElement = null; 
	for(int i = 0; i < messageElements.getLength(); i++){
	    String messageName = ((Element) messageElements.item(i)).getAttribute("name");

	    if(targetMessageName.equals(messageName)){
		// Find the message part, and its datatype
		NodeList partElements = ((Element) messageElements.item(i)).getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE, 
												   "part");
		if(partElements.getLength() != 1){
		    throw new Exception("The WSDL message (" + messageName+") containing the values " +
					"for a Moby secondary param (operation "+opName+") did not have one part " +
					"as expected, but rather "+partElements.getLength());
		}
		partElement = (Element) partElements.item(0);
		break;
	    }
	}
	if(partElement == null){
	    throw new Exception("Could not find a message definition (" + targetMessageName + 
				") for operation "+ opName);
	}
	// The lifting may be defined at the "part" tag level, as it is for services
	String schemaMapping = partElement.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_OUTMAP_ATTR);
	if(schemaMapping != null && schemaMapping.length() != 0){
	    return new URL(schemaMapping);
	}

	String partType = partElement.getAttribute("type");
	if(partType == null || partType.trim().length() == 0){
	    // See if it's document/literal, in which case we need the element reference
	    String elementType = partElement.getAttribute("element");
	    if(elementType == null || elementType.trim().length() == 0){
		throw new Exception("The WSDL message (" + targetMessageName+") containing the values " +
				    "for a Moby secondary param (operation "+opName+") has a part element " +
				    "as expected, but no defined data type ('type' or 'element' attribute " +
				    "missing, for rpc/encoded or doc/lit respectively)");
	    }
	    // Track back to the element xml schema definition for doc/lit: it should have the SAWSDL
	    partType = elementType;
	}
	// See if the name is namespace qualified, and separate the local part if so
	String partTypeNamespaceURI = "";
	if(partType.contains(":")){
	    String nsPrefix = partType.substring(0, partType.indexOf(":"));  //XML NS prefix
	    partType = partType.substring(partType.indexOf(":")+1); //local part
	    partTypeNamespaceURI = partElement.lookupNamespaceURI(nsPrefix);  //prefix->URI
	}

	// Now find the definition of the part's data type, and see if it has a schema lifting mapping
	// Usually, <xsd:schema> -> <xsd:complexType name="partType"/> for rpc/encoded, 
	//       or <element name="partType"><xsd:complexType><sequence><element> for doc/lit, in which case we need to dig further
	Element schemaDefElement = getSchemaElement(opElement.getOwnerDocument(), partTypeNamespaceURI, partType);
	schemaMapping = schemaDefElement.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_OUTMAP_ATTR);
	if((schemaMapping == null || schemaMapping.length() == 0) &&
	   "element".equals(schemaDefElement.getNodeName())){
	    NodeList subElements = schemaDefElement.getElementsByTagNameNS(MobyPrefixResolver.XSD_NAMESPACE, "element");
	    if(subElements == null || subElements.getLength() == 0){
		throw new Exception("The definition of XML Schema type " + partType + " used as the output of " + 
				    opName + " has no child elements, nor a SAWSDL " + SAWSDL_OUTMAP_ATTR + 
				    " attribute");
	    }
	    schemaDefElement = (Element) subElements.item(0);
	}

	// If it's an XML Schema definition element with the same name as our part type, we're good to go...
	// See if the SAWSDL lifting schema attribute is defined
	schemaMapping = schemaDefElement.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_OUTMAP_ATTR);
	if(schemaMapping == null || schemaMapping.length() == 0){
	    // As a last-ditch effort, if the data type is just an array of another datatype, 
	    // look up the other data type to see if it has a lifting schema mapping, and
	    // we will take care of the array iteration part of the transformation
	    String arrayType = getArrayType(schemaDefElement);
	    if(arrayType == null){
		throw new Exception("The definition of XML Schema type " + partType + " used as the output of " + 
				    opName + " has no SAWSDL " + SAWSDL_OUTMAP_ATTR + " attribute defined, nor is" +
				    "it simply an array of a type with a lifting schema");
	    }

	    // We're at the array's data type.  This is the last chance to find a schema lifting mapping
	    Element sde = getSchemaElement(opElement.getOwnerDocument(), partTypeNamespaceURI, arrayType);
	    schemaMapping = sde.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_OUTMAP_ATTR);

	    if(schemaMapping != null && schemaMapping.length() != 0){
		// The ARRAY_SENTINEL as a ref part of the URL indicates the rule should be
		// applied to each members of the incoming array, not the once to the whole array
		if(lsidResolver.isLSID(schemaMapping)){
		    return new URL(lsidResolver.resolveDataURL(schemaMapping).toString()+
				   "#"+ARRAY_SENTINEL);
		}
		else{
		    return new URL(schemaMapping+"#"+ARRAY_SENTINEL);
		}
	    }
	    throw new Exception("Neither the array datatype ("+partType + ") nor the datatype " +
				"it stores (" + arrayType + ") has a SAWSDL " + SAWSDL_OUTMAP_ATTR +
				" attribute");
	}
	else{
	    if(lsidResolver.isLSID(schemaMapping)){
		return lsidResolver.resolveDataURL(schemaMapping);
	    }
	    else{
		return new URL(schemaMapping);
	    } 
	}
    }

    /**
     * @return the data type of the array, or null if the data type is not an array
     */
    protected String getArrayType(Element schemaDefElement){
	// The XML must look something like:
	//<xsd:complexType name="ArrayOfThing">
	// <xsd:complexContent>
	//  <xsd:restriction base="soapenc:Array">
	//   <xsd:attribute ref="soapenc:arrayType" wsdl:arrayType="typens:Thing[]"/>
	// </xsd:restriction></xsd:complexContent></xsd:complexType>
	if(schemaDefElement == null){
	    return null;
	}
	NodeList contentElements = schemaDefElement.getElementsByTagNameNS(MobyPrefixResolver.XSD_NAMESPACE, 
									   "complexContent");
	if(contentElements.getLength() == 1){
	    NodeList restrictionElements = 
		((Element) contentElements.item(0)).getElementsByTagNameNS(MobyPrefixResolver.XSD_NAMESPACE, 
									   "restriction");
	    if(restrictionElements.getLength() == 1){
		NodeList attributeElements = ((Element) restrictionElements.item(0)).getElementsByTagNameNS(MobyPrefixResolver.XSD_NAMESPACE, 
													    "attribute");
		if(attributeElements.getLength() == 1){
		    // NOTE: WSDL 1.1 only for now
		    String arrayType = ((Element) attributeElements.item(0)).getAttributeNS(MobyPrefixResolver.WSDL_NAMESPACE, 
											    "arrayType");
		    if(arrayType.contains(":")){
			arrayType = arrayType.substring(arrayType.indexOf(":")+1); //local part
		    }
		    if(arrayType.endsWith("[]")){
			return arrayType.substring(0, arrayType.length()-2); // remove the array square brackets
		    }
		}
	    }
	}
	return null;
    }

    /**
     * Returns the XML DOM element containing the definition of an XML Schema data type, 
     * unless that datatype or namespace doesn't exist, in which case an exception is thrown.
     */
    protected Element getSchemaElement(Document wsdlDoc, String nsUri, String name) throws Exception{
	NodeList schemaElements = wsdlDoc.getDocumentElement().getElementsByTagNameNS(MobyPrefixResolver.XSD_NAMESPACE, 
										      "schema");
	if(schemaElements.getLength() == 0){
	    throw new Exception("Could not find XML Schema type definition for " + name + 
				", cannot find the schema section of the WSDL document");
	}

	for(int i = 0; i < schemaElements.getLength(); i++){
	    if(!nsUri.equals(((Element) schemaElements.item(i)).getAttribute("targetNamespace"))){
		continue; // only look as schema definitions in the correct namespace
	    }
	    
	    NodeList schemaDefElements = ((Element) schemaElements.item(i)).getChildNodes();
	    for(int j = 0; j < schemaDefElements.getLength(); j++){
		if(!(schemaDefElements.item(j) instanceof Element)){
		    continue;
		}
		Element e = (Element) schemaDefElements.item(j);
		if((MobyPrefixResolver.XSD_NAMESPACE.equals(e.getNamespaceURI())) &&
		   name.equals(e.getAttribute("name"))){
		    return e;
		}
	    }
	    throw new Exception("Could not find XML Schema type definition for " + name + ", namespace " +
				nsUri + " exists, but schema element " + name + " does not");
	}
	throw new Exception("Could not find XML Schema type definition for " + name + ", namespace " +
			    nsUri + " does not exist");
    }

    public static Element renameSoapArrayElements(Element arrayElement, String newName, Document owner){
	NodeList arrayElements = arrayElement.getChildNodes();
	Element renamedArray = owner.createElement("soap-array");
	for(int j = 0; j < arrayElements.getLength(); j++){
	    if(!(arrayElements.item(j) instanceof Element)){
		continue;
	    }
	    // For XPath rules to work properly, we need to rename the items 
	    // in the array to the parent name
	    Element renamedArrayItem = owner.createElement(newName);
	    // clone the old element attributes and children into the new one
	    Element oldElement = (Element) arrayElements.item(j);
	    NamedNodeMap attrs = oldElement.getAttributes();
	    for(int k = 0; k < attrs.getLength(); k++){
		renamedArrayItem.setAttributeNodeNS((Attr) attrs.item(k).cloneNode(true));
	    }
	    NodeList children = oldElement.getChildNodes();
	    for(int k = 0; k < children.getLength(); k++){
		renamedArrayItem.appendChild(children.item(k).cloneNode(true));
	    }
	    renamedArray.appendChild(renamedArrayItem);
	}
	return renamedArray;
    }

    protected String convertSourceToMobySecondaryValues(Source source, URL liftingSchema, boolean arrayRule) throws Exception{
	// The source will contain an XML document, lets convert it to a string of the form val1,val2,val3 using a stylesheet
	TransformerFactory transformerFactory = TransformerFactory.newInstance();
	Transformer transformer = null;
	//System.err.println("The lifting schema is " + liftingSchema);
	try{
	    // Create the transformer that'll turn the soap payload into a moby secondary spec string
	    transformer = transformerFactory.newTransformer(new StreamSource(liftingSchema.openStream()));
	} catch (TransformerConfigurationException tce){
	    logger.log(Level.SEVERE,
		       "Could not create an XSLT transformer: " + tce,
		       tce);
	}
	StringWriter stringWriter = new StringWriter();
	// Apply the rule to each member of the array, rather than just once to the array
	if(arrayRule){
	    // To simplify life (at the cost of inefficiency of processing), 
            // turn any source into a stream source (via a null XSLT transform) 
            // so we can parse it uniformily
	    if(!(source instanceof StreamSource)){
		StringWriter verbatim = new StringWriter();
		transformerFactory.newTransformer().transform(source, new StreamResult(verbatim));
		// Need to compensate for the fact that some XSLT transformers don't copy over the
		// namespace declaration properly, so declare it manually for xsi if that's the case
		// otherwise you get a parsing error later on when trying to do the 
		// xml schema -> moby xml transformation.  Ditto with soap encoding.
		String verb = verbatim.toString();
		if(verb.indexOf(" xmlns:xsi") == -1 && 
		   verb.indexOf(" xsi:") != -1){
		    verb = verb.replaceFirst(" xsi:", 
					     " xmlns:xsi=\""+MobyPrefixResolver.XSI_NAMESPACE2001+"\" xsi:");
		}
		if(verb.indexOf(" xmlns:SOAP-ENC") == -1 && 
		   verb.indexOf(" SOAP-ENC:") != -1){
		    verb = verb.replaceFirst(" SOAP-ENC:", 
					     " xmlns:SOAP-ENC=\""+MobyPrefixResolver.SOAP_ENC_NAMESPACE+"\" SOAP-ENC:");
		}
		source = new StreamSource(new StringReader(verb));
	    }

	    Document arrayDoc = null;
	    if(((StreamSource) source).getInputStream() != null){
		arrayDoc = docBuilder.parse(((StreamSource) source).getInputStream());
	    }
	    else if(((StreamSource) source).getReader() != null){
		arrayDoc = docBuilder.parse(new org.xml.sax.InputSource(((StreamSource) source).getReader()));
	    }
	    else{
		throw new Exception("Neither an InputStream nor a Reader was available from " +
				    "the StreamSource, cannot process the source");
	    }
	    Element arrayElement = (Element) arrayDoc.getDocumentElement().getFirstChild();
	    String dataTypeAttr = arrayElement.getAttributeNS(MobyPrefixResolver.SOAP_ENC_NAMESPACE, "arrayType");
	    String dataType = dataTypeAttr.replaceFirst("^.*:([^\\[]+).*", "$1");
	    //System.err.println("Array Data Type for secondary source is " + dataType);
	    // for RPC-encoded data, rename item tags to real data type name
	    Element renamedArray = renameSoapArrayElements(arrayElement, dataType, arrayDoc);  
	    NodeList arrayElements = renamedArray.getElementsByTagName(dataType);
	    for(int i = 0; i < arrayElements.getLength(); i++){
		if(i != 0){
		    stringWriter.write(',');  //join values with a comma
		}
		transformer.transform(new DOMSource(arrayElements.item(i)), new StreamResult(stringWriter));
	    }
	}
	// Not an array rule, just apply the stylesheet as-is
	else{
	    transformer.transform(source, new StreamResult(stringWriter));
	}
	//System.err.println("Response payload for secondary param:\n"+stringWriter.toString());

	return stringWriter.toString();
    }

    public static String join(Iterable i, String delimiter) {
        StringBuffer buffer = new StringBuffer();
	for(Object item: i){
            buffer.append(item.toString());
	    buffer.append(delimiter);
        }
        return buffer.substring(0, buffer.length()-delimiter.length());
    }

    // Finds out the soap operations to wrap and the associated Moby metadata such as service name, authority, etc.
    public void parseMobyServiceSpecs() throws Exception{
	NodeList serviceNameAttrs = (NodeList) xPath.evaluate(SERVICENAME_ATTR_XPATH,
							      wsdlDoc,
							      XPathConstants.NODESET);
	if(serviceNameAttrs == null || serviceNameAttrs.getLength() == 0){
	    throw new Exception("There do not appear to be any Moby serviceName attributes in the WSDL file.  " +
				"Either this is not a Moby-oriented SAWSDL file, or the Moby namespace is not properly defined.");
	}
	for(int i = 0; i < serviceNameAttrs.getLength(); i++){
	    Element el = (Element) serviceNameAttrs.item(i);
	    String serviceName = el.getAttributeNS(MobyPrefixResolver.MOBY_XML_NAMESPACE, SERVICE_NAME_ATTR);
	    if(serviceName == null || serviceName.trim().length() == 0){
		throw new Exception("The Moby service attribute " + SERVICE_NAME_ATTR + 
				    " is missing or blank for service " + serviceName);
	    }
	    if(!serviceName.trim().matches("[A-Za-z0-9_]+")){
		throw new Exception("The Moby service name attribute (" + serviceName + 
				    ") contains non-alphanumeric/underscore characters, " +
				    "which are illegal in Moby names");
	    }

	    // We expect the authority, contact and description in the same tag
	    //Element el = serviceAttr.getOwnerElement();
	    String serviceContact = el.getAttributeNS(MobyPrefixResolver.MOBY_XML_NAMESPACE, SERVICE_CONTACT_ATTR);
	    if(serviceContact == null || serviceContact.trim().length() == 0){
		throw new Exception("The Moby service attribute " + SERVICE_CONTACT_ATTR + 
				    " is missing or blank for service " + serviceName);
	    }
	    String serviceAuthority = el.getAttributeNS(MobyPrefixResolver.MOBY_XML_NAMESPACE, SERVICE_AUTH_ATTR);
	    if(serviceAuthority == null || serviceAuthority.trim().length() == 0){
		throw new Exception("The Moby service attribute " + SERVICE_AUTH_ATTR + 
				    " is missing or blank for service " + serviceName);
	    }
	    String serviceDescription = el.getAttributeNS(MobyPrefixResolver.MOBY_XML_NAMESPACE, SERVICE_DESC_ATTR);
	    if(serviceDescription == null || serviceDescription.trim().length() == 0){
		throw new Exception("The Moby service attribute " + SERVICE_DESC_ATTR + 
				    " is missing or blank for service " + serviceName);
	    }
	    String serviceCategoryLSID = el.getAttributeNS(MobyPrefixResolver.SAWSDL_NAMESPACE, SAWSDL_MODEL_ATTR);
	    if(serviceCategoryLSID == null || serviceCategoryLSID.trim().length() == 0){
		throw new Exception("The SAWSDL model reference attribute (" + SAWSDL_MODEL_ATTR + 
				    ") for the Moby service category is missing or blank for service " +
				    serviceName + " (there should " +
				    " be one in each element with a Moby " + SERVICE_NAME_ATTR + " attribute)");
	    }
	    String[] serviceCategoryLSIDFields = serviceCategoryLSID.split(":");
	    String serviceCategory = null;
	    if(serviceCategoryLSIDFields.length != 5 && serviceCategoryLSIDFields.length != 6 ||
	       !serviceCategoryLSIDFields[0].equals("urn") || !serviceCategoryLSIDFields[1].equals("lsid") ||
	       !serviceCategoryLSIDFields[3].equals("servicetype")){
		throw new Exception("The SAWSDL " + SAWSDL_MODEL_ATTR + " attribute for Moby service " + 
				    serviceName + " does not appear to be an LSID for as Moby Service Type (" +
				    "expected an attribute of the form \"urn:lsid:authority.org:servicetype:" +
				    "ServiceTypeName:optionalVersionField\").");
	    }
	    serviceCategory = serviceCategoryLSIDFields[4];

	    // The user can optionally pick the registry where the service will be published
	    // Later, the servlet code will check if the ontology terms used are valid within that registry
	    String registryEndpoint = el.getAttributeNS(MobyPrefixResolver.MOBY_XML_NAMESPACE, REGISTRY_ATTR);

	    // Set the vars for retrieval by the servlet, cleaning up errant whitespace where possible
	    currentService = serviceName.trim();
	    setContactEmail(serviceContact);
	    setCentralEndpoint(registryEndpoint == null ? null : registryEndpoint.trim());
	    setServiceType(serviceCategory.trim());
	    setProviderURI(serviceAuthority.trim());
	    setServiceDesc(serviceDescription.trim());	    

	    parseSOAPServiceSpecs(el);
	    
	}  // end for Moby service name attrs
    }

    // el is the tag containing the moby service attributes.  It lives in a WSDL operation hopefully...
    private void parseSOAPServiceSpecs(Element el) throws Exception{
	// Find the messages associated with the operation we're marking up: 
	// they will be checked later for semantic attrs.
	// In WSDL 2.0, the operation tag would be the current tag.  In WSDL 1.1, it'd be a parent
	// so we ascend the DOM until we find an "operation" element.	    
	Element opElement = el;
	for(; opElement != null; opElement = (Element) opElement.getParentNode()){
	    if("operation".equals(opElement.getLocalName()) && 
	       (MobyPrefixResolver.WSDL_NAMESPACE.equals(opElement.getNamespaceURI()) ||
		MobyPrefixResolver.WSDL20_NAMESPACE.equals(opElement.getNamespaceURI()))){
		break;
	    }
	}
	if(opElement == null){
	    throw new Exception("Could not find a WSDL 'operation' element enclosing the definition " +
				"of Moby service " + currentService);
	}
	
	String soapOpName = opElement.getAttribute("name");
	if(soapOpName == null || soapOpName.trim().length() == 0){
	    throw new Exception("The name of the WSDL 'operation' element enclosing the definition " +
				"of Moby service " + currentService + " is missing or blank");
	}
	soapOpName = soapOpName.trim();

	String soapAction = null;
	NodeList soapOps =
	    opElement.getElementsByTagNameNS("http://schemas.xmlsoap.org/wsdl/soap/",
					     "operation");
	for(int n = 0; n < soapOps.getLength(); n++){
	    Element soapOp = (Element) soapOps.item(n);
	    soapAction = soapOp.getAttribute("soapAction");
	    if(soapAction != null && soapAction.trim().length() > 0){					
		break;
	    }
	}

	QName[] names = getServiceAndPortFromOperation(opElement, soapOpName);
	QName[] ioMsgNames = getInputAndOutputMessageNamesFromOperation(opElement);

	String style = names[2].getNamespaceURI(); //we abused the QName structure to pass back op style/encoding info
	String encoding = names[2].getLocalPart(); //we abused the QName structure to pass back op style/encoding info

	setServiceQName(names[0]);
	setPortQName(names[1]);
	setSoapAction(soapAction);
	setOperationStyle(style);
	setOperationEncoding(encoding);
	setOperationName(soapOpName);
	setOperationInputQName(ioMsgNames[0]);
	setOperationOutputQName(ioMsgNames[1]);
    }

    /**
     * @return a two element array, with input message name, then output message name
     */
    protected QName[] getInputAndOutputMessageNamesFromOperation(Element opElement) throws Exception{
	// Now that we have the operation, find the input and output children we need to note
	NodeList inputElements = opElement.getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE, "input");
	if(inputElements == null || inputElements.getLength() == 0){
	    inputElements = opElement.getElementsByTagNameNS(MobyPrefixResolver.WSDL20_NAMESPACE, "input");
	}
	if(inputElements == null || inputElements.getLength() == 0){
	    throw new Exception("Could not find the WSDL input element descendant of the operation " +
				"element defining the Moby service " + currentService);
	}
	if(inputElements.getLength() > 1){
	    throw new Exception("More than one WSDL input element is defined for the operation " +
				"element defining the Moby service " + currentService);
	}
	
	NodeList outputElements = opElement.getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE, "output");
	if(outputElements == null || outputElements.getLength() == 0){
	    outputElements = opElement.getElementsByTagNameNS(MobyPrefixResolver.WSDL20_NAMESPACE, "output");
	}
	if(outputElements == null || outputElements.getLength() == 0){
	    throw new Exception("Could not find the WSDL output element descendant of the operation " +
				"element defining the Moby service " + currentService);
	}
	if(outputElements.getLength() > 1){
	    throw new Exception("More than one WSDL output element is defined for the operation " +
				"element defining the Moby service " + currentService);
	}

	// Should have element or message attr for WSDL 2.0 and 1.1 respectively
	String inputName = ((Element) inputElements.item(0)).getAttribute("message");
	if(inputName == null || inputName.trim().length() == 0){
	    inputName = ((Element) inputElements.item(0)).getAttribute("element");
	}
	if(inputName == null || inputName.trim().length() == 0){
	    throw new Exception("Could not find the message or element attribute associated with " +
				"the WSDL input element for the Moby service " + currentService);
	}
	String nsURI = null;
	if(inputName.contains(":")){  // convert ns:label to QName object
	    String nsPrefix = inputName.substring(0, inputName.indexOf(":"));  //XML NS prefix
	    inputName = inputName.substring(inputName.indexOf(":")+1); //local part
	    nsURI = inputElements.item(0).lookupNamespaceURI(nsPrefix);  //prefix->URI
	}
	QName inputQName = new QName(nsURI, inputName);

	// Should have element or message attr for WSDL 2.0 and 1.1 respectively
	String outputName = ((Element) outputElements.item(0)).getAttribute("message");
	if(outputName == null || outputName.trim().length() == 0){
	    outputName = ((Element) outputElements.item(0)).getAttribute("element");
	}
	if(outputName == null || outputName.trim().length() == 0){
	    throw new Exception("Could not find the message or element attribute associated with " +
				"the WSDL output element for the Moby service " + currentService);
	}
	if(outputName.contains(":")){  // convert ns:label to QName object
	    String nsPrefix = outputName.substring(0, outputName.indexOf(":"));  //XML NS prefix
	    outputName = outputName.substring(outputName.indexOf(":")+1); //local part
	    nsURI = outputElements.item(0).lookupNamespaceURI(nsPrefix);  //prefix->URI
	}
	QName outputQName = new QName(nsURI, outputName);

	return new QName[]{inputQName, outputQName};
    }

    /**
     * @return a two element array, with service, then port, then style
     */
    protected QName[] getServiceAndPortFromOperation(Element opElement, String soapOpName) throws Exception{

	// Now go up the DOM to find the port type (WSDL 1.1) or interface (WSDL 2.0) enclosing the operation
	Element porttypeInterfaceElement = (Element) opElement.getParentNode();
	for(; porttypeInterfaceElement != null; 
	    porttypeInterfaceElement = (Element) porttypeInterfaceElement.getParentNode()){
	    if("portType".equals(porttypeInterfaceElement.getLocalName()) && 
	       MobyPrefixResolver.WSDL_NAMESPACE.equals(opElement.getNamespaceURI()) ||
	       "interface".equals(porttypeInterfaceElement.getLocalName()) &&
	       MobyPrefixResolver.WSDL20_NAMESPACE.equals(opElement.getNamespaceURI())){
		break;
	    }
	}
	if(porttypeInterfaceElement == null){
	    throw new Exception("Could not find a WSDL 1.1 'portType' or WSDL 2.0 " +
				"'interface' element enclosing the " + soapOpName +
				" operation");
	}
	String portTypeName = porttypeInterfaceElement.getAttribute("name");
	if(portTypeName == null || portTypeName.length() == 0){
	    throw new Exception("The port/interface element enclosing the " + soapOpName + 
				" operation in the WSDL file does not have a name");
	}

	// I think the WSDL 2.0 model is very different from this point on...must investigate!

	// Find the binding that use the portType
	NodeList bindingElements = wsdlDoc.getDocumentElement().getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE,
										       "binding");
	if(bindingElements == null || bindingElements.getLength() == 0){
	    throw new Exception("Could not find any WSDL binding elements in the WSDL");
	}
	String bindingName = null;
	String style = null;
	String encoding = null;
	for(int i = 0; i < bindingElements.getLength(); i++){
	    Element bindingElement = (Element) bindingElements.item(i);
	    String bindingPortType = bindingElement.getAttribute("type");
	    if(portTypeName.equals(bindingPortType) || 
	       (bindingPortType != null && bindingPortType.endsWith(":"+portTypeName))){
		// name matches, or maybe name with a prefix:
		bindingName = bindingElement.getAttribute("name");
		if(bindingName == null || bindingName.length() == 0){
		    throw new Exception("The WSDL binding element that uses the " + portTypeName + 
					" port type does not have a name attribute.");
		}

		// find the style of the soap op (document or rpc)
		NodeList opElements = bindingElement.getElementsByTagNameNS("http://schemas.xmlsoap.org/wsdl/soap/",
									    "binding");
		for(int j = 0; j < opElements.getLength(); j++){
		    Element op = (Element) opElements.item(j);
		    style = op.getAttribute("style");
		    if(style != null && style.length() > 0){
			break;
		    }
		}

		// find the encoding type (encoded or literal) for the soap op
		opElements = bindingElement.getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE,
								   "operation");
		for(int j = 0; j < opElements.getLength(); j++){
		    Element op = (Element) opElements.item(j);
		    String opName = op.getAttribute("name");
		    if(soapOpName.equals(opName)){
			NodeList inputs =
			    op.getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE,
						      "input");
			String inputMsgName = null;
			QName inputMsgQName = null;
			for(int n = 0; n < inputs.getLength(); n++){
			    Element input = (Element) inputs.item(n);
			    inputMsgName = input.getAttribute("name");
			    
			    NodeList soapInputs =
				input.getElementsByTagNameNS("http://schemas.xmlsoap.org/wsdl/soap/",
							     "body");
			    if(soapInputs == null || soapInputs.getLength() == 0){
				throw new Exception("Could not find a SOAP body definition for operation " + opName + "\n");
			    }
			    Element bodyDef = (Element) soapInputs.item(0);
			    encoding = bodyDef.getAttribute("use");
			    if(encoding != null && encoding.length() != 0){
				break;
			    }
			}
			break;
		    }
		}
	    }
	}
	if(bindingName == null){
	    throw new Exception("Could not find the WSDL binding element that uses the " + soapOpName + " operation's " +
				"port type (" + portTypeName + ")");
	}

	// Find the port that uses the binding
	NodeList portElements = wsdlDoc.getDocumentElement().getElementsByTagNameNS(MobyPrefixResolver.WSDL_NAMESPACE,
										    "port");
	if(portElements == null || portElements.getLength() == 0){
	    throw new Exception("Could not find any WSDL port elements in the WSDL");
	}
	String portName = null;
	Element portElement = null;
	for(int i = 0; i < portElements.getLength(); i++){
	    portElement = (Element) portElements.item(i);
	    String bName = portElement.getAttribute("binding");
	    if(bindingName.equals(bName) || 
	       (bName != null && bName.endsWith(":"+bindingName))){
		// name matches, or maybe name with a prefix:
		portName = portElement.getAttribute("name");
		if(portName == null || portName.length() == 0){
		    throw new Exception("The WSDL port element that uses the " + bindingName + 
					" binding does not have a name attribute.");
		}
		break;
	    }
	}
	if(portName == null){
	    throw new Exception("Could not find the WSDL port element that uses the " + soapOpName + " operation's " +
				"binding (" + bindingName + ")");
	}	

	// Find the service that uses the port, it must be a parent element.
	Element serviceElement = (Element) portElement.getParentNode();
	for(; serviceElement != null; serviceElement = (Element) serviceElement.getParentNode()){
	    if("service".equals(serviceElement.getLocalName()) && 
	       (MobyPrefixResolver.WSDL_NAMESPACE.equals(opElement.getNamespaceURI()) ||
		MobyPrefixResolver.WSDL20_NAMESPACE.equals(opElement.getNamespaceURI()))){
		break;
	    }
	}
	if(serviceElement == null){
	    throw new Exception("Could not find a WSDL 'service' element enclosing the port " + portName);
	}
	String serviceName = serviceElement.getAttribute("name");
	if(serviceName == null || serviceName.length() == 0){
	    throw new Exception("The WSDL service element enclosing the port " + portName + 
				" does not have a name attribute");
	}	

	// find the target namespace for the service and port, as JAX-WS will need these later
	String targetNamespace = wsdlDoc.getDocumentElement().getAttributeNS(MobyPrefixResolver.WSDL_NAMESPACE, 
									     "targetNamespace");
	if(targetNamespace == null || targetNamespace.length() == 0){
	    targetNamespace = wsdlDoc.getDocumentElement().getAttribute("targetNamespace");
	}
	if(targetNamespace == null || targetNamespace.length() == 0){
	    throw new Exception("No targetNamespace attribute was found in the root element of the WSDL document");
	}

	QName[] specs = new QName[3];
	specs[0] = new QName(targetNamespace, serviceName);
	specs[1] = new QName(targetNamespace, portName);
	// not really a QName, but pass it back as such for simplicity (will be either 
	// "document" or "rpc", and "literal" or "encoded")
	specs[2] = new QName(style, encoding);  
	return specs;
    }

    public String getOperationName(){
	return mobyServiceName2Op.get(currentService);
    }

    public void setOperationName(String opName){
	mobyServiceName2Op.put(currentService, opName);
    }

    public QName getPortQName(){
	return mobyServiceName2Port.get(currentService);
    }

    public void setPortQName(QName portName){
	mobyServiceName2Port.put(currentService, portName);
    }

    public QName getServiceQName(){
	return mobyServiceName2Service.get(currentService);
    }

    public void setServiceQName(QName serviceName){
	mobyServiceName2Service.put(currentService, serviceName);
    }

    public QName getOperationInputQName(){
	return mobyServiceName2OpInput.get(currentService);
    }

    public void setOperationInputQName(QName inputName){
	mobyServiceName2OpInput.put(currentService, inputName);
    }

    /**
     * If style is not explicitly available, literal is the default
     */
    public String getOperationEncoding(){
	String encoding = mobyServiceName2Encoding.get(currentService);
	if(encoding == null){
	    return "literal";
	}
	return encoding;
    }

    public void setOperationEncoding(String encoding){
	mobyServiceName2Encoding.put(currentService, encoding);
    }

    public String getOperationStyle(){
	String style = mobyServiceName2Style.get(currentService);
	if(style == null){
	    return "document";
	}
	return style;
    }

    public void setOperationStyle(String style){
	mobyServiceName2Style.put(currentService, style);
    }

    public String getSoapAction(){
	return mobyServiceName2SoapAction.get(currentService);
    }

    public void setSoapAction(String soapAction){
	mobyServiceName2SoapAction.put(currentService, soapAction);
    }

    public QName getOperationOutputQName(){
	return mobyServiceName2OpOutput.get(currentService);
    }

    public void setOperationOutputQName(QName outputName){
	mobyServiceName2OpOutput.put(currentService, outputName);
    }

    public Map<String,String> getInputXSDTypes(){
	return mobyServiceName2InputXSDTypes.get(currentService);
    }

    public void setInputXSDTypes(Map<String,String> types){
	mobyServiceName2InputXSDTypes.put(currentService, types);
    }
    public Map<String,String> getOutputXSDTypes(){
	return mobyServiceName2OutputXSDTypes.get(currentService);
    }

    public void setOutputXSDTypes(Map<String,String> types){
	mobyServiceName2OutputXSDTypes.put(currentService, types);
    }

    public String getTargetNamespaceURI(){
	return mobyServiceName2TargetNamespaceURI.get(currentService);
    }

    public void setTargetNamespaceURI(String nsURI){
	mobyServiceName2TargetNamespaceURI.put(currentService, nsURI);
    }

}

