Invalid wsdl generated by spring-ws when the request element doesn't end with 'Request'

Dariusz picture Dariusz · Dec 30, 2015 · Viewed 8.1k times · Source

I must prepare a webservice to accept anan already defined wsdl structure. I followed the tutorial found here, with source code for the test project downloadable here.

For xsd like this:

<xs:element name="getCountryRequest">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

Wsdl operation for request returned by application is OK, looks like this:

<wsdl:binding name="CountriesPortSoap11" type="tns:CountriesPort">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="getCountry">
        <soap:operation soapAction=""/>
        <wsdl:input name="getCountryRequest">
            <soap:body use="literal"/>
        </wsdl:input>
        <wsdl:output name="getCountryResponse">
            <soap:body use="literal"/>
        </wsdl:output>
    </wsdl:operation>
</wsdl:binding>

But when I change the xsd to (no 'Request' in element name):

<xs:element name="getCountry">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

the wsdl is invalid, and has no <input> specified:

<wsdl:binding name="CountriesPortSoap11" type="tns:CountriesPort">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="getCountry">
        <soap:operation soapAction=""/>
        <wsdl:output name="getCountryResponse">
            <soap:body use="literal"/>
        </wsdl:output>
    </wsdl:operation>
</wsdl:binding>

How can I fix that? How can I make a Request-less element appear properly as soap operation input in wsdl?

Answer

A_Di-Matteo picture A_Di-Matteo · Jan 2, 2016

According to official Spring WS documentation, the Request/Response suffix are the default ones used to automatically determine request/response and as such generate the expected WSDL.

The DefaultWsdl11Definition which builds a WSDL from a XSD schema. This definition iterates over all element elements found in the schema, and creates a message for all elements. Next, it creates WSDL operation for all messages that end with the defined request or response suffix. The default request suffix is Request; the default response suffix is Response, though these can be changed by setting the requestSuffix and responseSuffix properties, respectively.

You can hence, in the mentioned example code, change the suffix in the WebServiceConfig configuration class, defaultWsdl11Definition method, adding the following method invocation:

wsdl11Definition.setRequestSuffix("your-new-prefix-here");

You can, for instance, set it to Req instead of Request, the build will then automatically generate a new GetCountryReq class, code of ApplicationTests and CountryEndpoint would then need to be manually adapted, removing compilation errors (as they would still point to the previously existing GetCountryRequest class) but also making sure to change the localPart = "getCountryReq" attribute of the @PayloadRoot annotation in the CountryEndPoint class.

I tried it and build went fine and WSDL was updated accordingly.

That's about changing the default suffix to another suffix. But what about changing it to an empty suffix?

wsdl11Definition.setRequestSuffix("");

Exception: suffix must not be empty. Spring does not support it. According to this thread:

Basically, the problem is this:
We have to differentiate which schema elements are wsdl messages, and which aren't.
Of all wsdl messages, we have to figure out which are input (request) messages.
Of all wsdl messages, we have to figure out which are output (response) messages.

The DefaultWsdl11Definition figures this out by suffixes. Or, more specifically, it delegates to a SuffixBasedMessagesProvider and SuffixBasedPortTypesProvider to do so.
So if you have some other preferred way of determining what makes an input/output message, you will have to write your own messagesprovider and or porttypesprovider.

Simply put: there is no generic way for Spring-WS to determine what makes up a request and a response, rather than using suffixes.

Note: the poster of this message was Arjen Poutsma, author of the DefaultWsdl11Definition class (according to javadocs), the component which handles the automatic mapping based on these suffix conventions.

But he leaves an open door: writing your own SuffixBasedMessagesProvider and SuffixBasedPortTypesProvider. However, he also left everything as private in the DefaultWsdl11Definition (where these providers are instantiated), hence you would also need to write (copy) your own WSDL11 definition mapper.

Here is the process I followed then:

  • Create your own CustomSuffixBasedMessagesProvider, overriding the setRequestSuffix method and removing the check on empty suffix, in the isMessageElement method you would need to handle the new mapping
  • Create your own CustomSuffixBasedPortTypesProvider, overriding the setRequestSuffix method and removing the check on empty suffix, in the getOperationName and isInputMessage methods you would need to handle the new mapping
  • Create your own CustomWsdl11Definition as a copy of the existing DefaultWsdl11Definition and instantiating your own providers created above
  • Change the WebServiceConfig class, defaultWsdl11Definition method, to use your own CustomWsdl11Definition in order to apply the whole customization.

However, empty suffix comes with a bit of challenge, since it would be fine for any element (that is, every element has an empty suffix). That's why I mentioned the isMessageElement, isInputMessage and getOperationName handling: on growing WSDLs, you may easily end up on harcoding the mapping (for GetCountry request, GetCountryResponse is the response) or passing a properties/map (as suggested in the thread mentioned above), but again repeating most of your XSD again in your WebServiceConfig configuration class, making it hard to maintain, troubleshoot, share.

So, I would really suggest not to take this journey and either stick to the default suffix (if possible) or create a new one, but avoid empty suffix (they are not allowed by the library after all).

But since I took the journey, here is the working solution:

The MySuffixBasedMessagesProvider custom class (imports removed for brevity):

package hello;

public class MySuffixBasedMessagesProvider extends SuffixBasedMessagesProvider {

    protected String requestSuffix = DEFAULT_REQUEST_SUFFIX;

    public String getRequestSuffix() {
        return this.requestSuffix;
    }

    public void setRequestSuffix(String requestSuffix) {
        this.requestSuffix = requestSuffix;
    }

    @Override
    protected boolean isMessageElement(Element element) {
        if (isMessageElement0(element)) {
            String elementName = getElementName(element);
            Assert.hasText(elementName, "Element has no name");
            return elementName.endsWith(getResponseSuffix())
                    || (getRequestSuffix().isEmpty() ? true : elementName.endsWith(getRequestSuffix()))
                    || elementName.endsWith(getFaultSuffix());
        }
        return false;
    }

    protected boolean isMessageElement0(Element element) {
        return "element".equals(element.getLocalName())
                && "http://www.w3.org/2001/XMLSchema".equals(element.getNamespaceURI());
    }
}

The MySuffixBasedPortTypesProvider custom class (imports removed for brevity):

package hello;

public class MySuffixBasedPortTypesProvider extends SuffixBasedPortTypesProvider {

    private String requestSuffix = DEFAULT_REQUEST_SUFFIX;

    public String getRequestSuffix() {
        return requestSuffix;
    }

    public void setRequestSuffix(String requestSuffix) {
        this.requestSuffix = requestSuffix;
    }

    @Override
    protected String getOperationName(Message message) {
        String messageName = getMessageName(message);
        String result = null;
        if (messageName != null) {
            if (messageName.endsWith(getResponseSuffix())) {
                result = messageName.substring(0, messageName.length() - getResponseSuffix().length());
            } else if (messageName.endsWith(getFaultSuffix())) {
                result = messageName.substring(0, messageName.length() - getFaultSuffix().length());
            } else if (messageName.endsWith(getRequestSuffix())) {
                result = messageName.substring(0, messageName.length() - getRequestSuffix().length());
            }
        }
        return result;
    }

    @Override
    protected boolean isInputMessage(Message message) {
        String messageName = getMessageName(message);

        return messageName != null && !messageName.endsWith(getResponseSuffix());
    }

    private String getMessageName(Message message) {
        return message.getQName().getLocalPart();
    }

}

The MyWsdl11Definition custom class (essentially a copy of the Default one, just instantiating the classes above, imports removed for brevity):

package hello;

public class MyWsdl11Definition implements Wsdl11Definition, InitializingBean {

    private final InliningXsdSchemaTypesProvider typesProvider = new InliningXsdSchemaTypesProvider();

    private final SuffixBasedMessagesProvider messagesProvider = new MySuffixBasedMessagesProvider();

    private final SuffixBasedPortTypesProvider portTypesProvider = new MySuffixBasedPortTypesProvider();

    private final SoapProvider soapProvider = new SoapProvider();

    private final ProviderBasedWsdl4jDefinition delegate = new ProviderBasedWsdl4jDefinition();

    private String serviceName;

    public MyWsdl11Definition() {
        delegate.setTypesProvider(typesProvider);
        delegate.setMessagesProvider(messagesProvider);
        delegate.setPortTypesProvider(portTypesProvider);
        delegate.setBindingsProvider(soapProvider);
        delegate.setServicesProvider(soapProvider);
    }

    public void setTargetNamespace(String targetNamespace) {
        delegate.setTargetNamespace(targetNamespace);
    }

    public void setSchema(XsdSchema schema) {
        typesProvider.setSchema(schema);
    }

    public void setSchemaCollection(XsdSchemaCollection schemaCollection) {
        typesProvider.setSchemaCollection(schemaCollection);
    }

    public void setPortTypeName(String portTypeName) {
        portTypesProvider.setPortTypeName(portTypeName);
    }

    public void setRequestSuffix(String requestSuffix) {
        portTypesProvider.setRequestSuffix(requestSuffix);
        messagesProvider.setRequestSuffix(requestSuffix);
    }

    public void setResponseSuffix(String responseSuffix) {
        portTypesProvider.setResponseSuffix(responseSuffix);
        messagesProvider.setResponseSuffix(responseSuffix);
    }

    public void setFaultSuffix(String faultSuffix) {
        portTypesProvider.setFaultSuffix(faultSuffix);
        messagesProvider.setFaultSuffix(faultSuffix);
    }

    public void setCreateSoap11Binding(boolean createSoap11Binding) {
        soapProvider.setCreateSoap11Binding(createSoap11Binding);
    }

    public void setCreateSoap12Binding(boolean createSoap12Binding) {
        soapProvider.setCreateSoap12Binding(createSoap12Binding);
    }

    public void setSoapActions(Properties soapActions) {
        soapProvider.setSoapActions(soapActions);
    }

    public void setTransportUri(String transportUri) {
        soapProvider.setTransportUri(transportUri);
    }

    public void setLocationUri(String locationUri) {
        soapProvider.setLocationUri(locationUri);
    }

    public void setServiceName(String serviceName) {
        soapProvider.setServiceName(serviceName);
        this.serviceName = serviceName;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (!StringUtils.hasText(delegate.getTargetNamespace()) && typesProvider.getSchemaCollection() != null &&
                typesProvider.getSchemaCollection().getXsdSchemas().length > 0) {
            XsdSchema schema = typesProvider.getSchemaCollection().getXsdSchemas()[0];
            setTargetNamespace(schema.getTargetNamespace());
        }
        if (!StringUtils.hasText(serviceName) && StringUtils.hasText(portTypesProvider.getPortTypeName())) {
            soapProvider.setServiceName(portTypesProvider.getPortTypeName() + "Service");
        }
        delegate.afterPropertiesSet();
    }

    @Override
    public Source getSource() {
        return delegate.getSource();
    }

}

Furthermore, the defaultWsdl11Definition method of the WebServiceConfig class would change as following (to use the customization above):

@Bean(name = "countries")
public Wsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) {
    MyWsdl11Definition wsdl11Definition = new MyWsdl11Definition();
    wsdl11Definition.setPortTypeName("CountriesPort");
    wsdl11Definition.setLocationUri("/ws");
    wsdl11Definition.setRequestSuffix("");
    wsdl11Definition.setTargetNamespace("http://spring.io/guides/gs-producing-web-service");
    wsdl11Definition.setSchema(countriesSchema);
    return wsdl11Definition;
}

Note the wsdl11Definition.setRequestSuffix(""); which effectively sets the suffix at empty.

After this customization, you can then change the XSD removing the Request suffix, the new GetCountry class would be generated, you would need to manually remove the previously existing GetCountryRequest and fix the compilation errors as mentioned above (test and endpoint class, just don't forget to update the @PayloadRoot annotation as well).

Build would then run fine. Running the Application main, the generated WSDL would then contain as requested:

<wsdl:binding name="CountriesPortSoap11" type="tns:CountriesPort">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getCountry">
  <soap:operation soapAction=""/>
  <wsdl:input name="getCountry">
    <soap:body use="literal"/>
  </wsdl:input>
  <wsdl:output name="getCountryResponse">
    <soap:body use="literal"/>
  </wsdl:output>
</wsdl:operation>
</wsdl:binding>

Hope it helps. This is a real example of what convention over configuration greatly provides and what instead a small unforeseen change in a framework would then mean in terms of writing code and adding customization.