According to ValidationFeature documentation in order for the validation to happen the operations input and output bindings must be annotated with @Valid
However, the webservice interface generated by cxf-codegen-plugin does not have these annotations, and I don't seem to find a command line argument or a plugin that allows to add them.
The @Valid
annotations cannot be put in the implementation of the webservice interface without violating Liskov substitution principle: the reference implementation of JSR-349 (Hibernate Validator) in this case produces HV000151: A method overriding another method must not alter the parameter constraint configuration
Question: Is anybody aware of a way to annotate the cxf-generated webservice interface method parameters with @Valid
?
I'm aware of the existance of the Annox plugin but this does not seems to be an easy task to accomplish with it.
The easiest solution possible would be to manually add the @Valid
annotation to the webservice interface but I'm not comfortable in modifying generated code
Example
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example.www</groupId>
<artifactId>webservice-test-bval</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Test bean validation on web service</name>
<properties>
<org.springframework.boot.version>1.4.2.RELEASE</org.springframework.boot.version>
<com.github.krasa.krasa-jaxb-tools>1.5</com.github.krasa.krasa-jaxb-tools>
<org.apache.cxf.version>3.1.3</org.apache.cxf.version>
<cxf-codegen-plugin.version>3.0.1</cxf-codegen-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${org.springframework.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- CXF dependencies -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>${org.apache.cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>${org.apache.cxf.version}</version>
</dependency>
<!-- Schema validation -->
<dependency>
<groupId>com.github.krasa</groupId>
<artifactId>krasa-jaxb-tools</artifactId>
<version>${com.github.krasa.krasa-jaxb-tools}</version>
<exclusions>
<exclusion>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${org.springframework.boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf-codegen-plugin.version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>${project.build.directory}/generated/</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${project.basedir}/src/main/resources/wsdl/test.wsdl</wsdl>
<wsdlLocation>classpath:wsdl/test.wsdl</wsdlLocation>
<extraargs>
<extraarg>-xjc-XJsr303Annotations</extraarg>
</extraargs>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.krasa</groupId>
<artifactId>krasa-jaxb-tools</artifactId>
<version>${com.github.krasa.krasa-jaxb-tools}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
src/main/resources/wsdl/
test.wsdl
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.example.org/test/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
name="test" targetNamespace="http://www.example.org/test/">
<wsdl:types>
<xsd:schema targetNamespace="http://www.example.org/test/">
<xsd:element name="NewOperation">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="in">
<xsd:simpleType>
<xsd:restriction base="xsd:int">
<xsd:minInclusive value="10" />
<xsd:maxInclusive value="20" />
</xsd:restriction>
</xsd:simpleType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="NewOperationResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="out">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:pattern value="[A-Z]+" />
</xsd:restriction>
</xsd:simpleType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</wsdl:types>
<wsdl:message name="NewOperationRequest">
<wsdl:part element="tns:NewOperation" name="parameters" />
</wsdl:message>
<wsdl:message name="NewOperationResponse">
<wsdl:part element="tns:NewOperationResponse" name="parameters" />
</wsdl:message>
<wsdl:portType name="testWS">
<jaxws:bindings xmlns:jaxws="http://java.sun.com/xml/ns/jaxws">
<jaxws:enableWrapperStyle>false</jaxws:enableWrapperStyle>
</jaxws:bindings>
<wsdl:operation name="NewOperation">
<wsdl:input message="tns:NewOperationRequest" />
<wsdl:output message="tns:NewOperationResponse" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="testWSSOAP" type="tns:testWS">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="NewOperation">
<soap:operation soapAction="http://www.example.org/test/NewOperation" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="testWS">
<wsdl:port binding="tns:testWSSOAP" name="testWSSOAP">
<soap:address location="http://www.example.org/" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
src/main/java/
org.example.test
package org.example.test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.support.SpringBootServletInitializer;
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package org.example.test;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
import javax.validation.Valid;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.ws.RequestWrapper;
import javax.xml.ws.ResponseWrapper;
import org.example.test.ObjectFactory;
@WebService(targetNamespace = "http://www.example.org/test/", name = "testWS")
@XmlSeeAlso({ObjectFactory.class})
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface TestWSValid {
@WebMethod(operationName = "NewOperation", action = "http://www.example.org/test/NewOperation")
@Valid @WebResult(name = "NewOperationResponse", targetNamespace = "http://www.example.org/test/", partName = "parameters")
public NewOperationResponse newOperation(
@Valid @WebParam(partName = "parameters", name = "NewOperation", targetNamespace = "http://www.example.org/test/")
NewOperation parameters
);
}
org.example.test.configuration
package org.example.test.configuration;
import javax.xml.ws.Endpoint;
import org.apache.cxf.Bus;
import org.apache.cxf.feature.Feature;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.apache.cxf.validation.BeanValidationFeature;
import org.example.test.services.TestWSImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource({ "classpath:META-INF/cxf/cxf.xml", "classpath:META-INF/cxf/cxf-servlet.xml" })
@ComponentScan({ "org.example.test" })
public class ApplicationConfiguration {
@Autowired
private Bus cxfBus;
@Bean
public Endpoint testWSEndpoint() {
EndpointImpl endpoint = new EndpointImpl(cxfBus, new TestWSImpl());
endpoint.setAddress("/testws");
endpoint.publish();
return endpoint;
}
@Bean
public ServletRegistrationBean cxfServlet() {
ServletRegistrationBean servlet = new ServletRegistrationBean(new CXFServlet(), "/services/*");
servlet.setLoadOnStartup(1);
return servlet;
}
@Bean
public Feature validationFeature() {
Feature validationFeature = new BeanValidationFeature();
validationFeature.initialize(cxfBus);
cxfBus.getFeatures().add(validationFeature);
ConstraintViolationInterceptor interceptor = new ConstraintViolationInterceptor();
cxfBus.getInFaultInterceptors().add(interceptor);
cxfBus.getOutFaultInterceptors().add(interceptor);
cxfBus.getProperties().put("exceptionMessageCauseEnabled", true);
return validationFeature;
}
}
package org.example.test.configuration;
import java.text.MessageFormat;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolationException;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor;
import org.apache.cxf.binding.soap.interceptor.Soap11FaultOutInterceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.Phase;
public class ConstraintViolationInterceptor extends AbstractSoapInterceptor {
public ConstraintViolationInterceptor() {
super(Phase.MARSHAL);
getBefore().add(Soap11FaultOutInterceptor.class.getName());
}
private static final String TEMPLATE = "[{0}] {1} : {2}";
@Override
public void handleMessage(SoapMessage message) throws Fault {
Fault fault = (Fault) message.getContent(Exception.class);
Throwable exception = fault.getCause();
if (exception instanceof ConstraintViolationException) {
fault.setMessage(processConstraints((ConstraintViolationException) exception));
}
}
private String processConstraints(ConstraintViolationException exception) {
return exception.getConstraintViolations().stream().map((error) -> {
return MessageFormat.format(TEMPLATE, error.getPropertyPath(), error.getMessage(), error.getInvalidValue());
}).collect(Collectors.joining(System.lineSeparator()));
}
}
org.example.test.services
package org.example.test.services;
import javax.jws.WebService;
import org.example.test.NewOperation;
import org.example.test.NewOperationResponse;
import org.example.test.ObjectFactory;
import org.example.test.TestWS;
@WebService(endpointInterface = "org.example.test.TestWS", portName = "TestWSPort", serviceName = "TestWS", targetNamespace = "http://www.example.org/test/")
public class TestWSImpl implements TestWS {
@Override
public NewOperationResponse newOperation(NewOperation parameters) {
int in = parameters.getIn();
NewOperationResponse response = new ObjectFactory().createNewOperationResponse();
if (in < 10 || in > 20) {
response.setOut("no no no");
} else {
response.setOut("OK");
}
return response;
}
}
With reference to the project above, you can test that as long as TestWSImpl implements TestWS
(the generated class) there is no validation occurring, but if TestWSImpl implements TestWSValid
(the class that contains the generated code with the @Valid
addition) then the validation works as expected