digital illusion
digital illusion

Reputation: 497

cxf-codegen-plugin does not comply with cxf ValidationFeature

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

Upvotes: 1

Views: 5074

Answers (2)

digital illusion
digital illusion

Reputation: 497

For the record, here is what I think it's best for CustomSEIGenerator implementation:

package org.example.test;

import java.util.List;
import java.util.Map;

import javax.validation.Valid;
import javax.xml.namespace.QName;

import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.tools.common.ToolContext;
import org.apache.cxf.tools.common.ToolException;
import org.apache.cxf.tools.common.model.JAnnotation;
import org.apache.cxf.tools.common.model.JavaInterface;
import org.apache.cxf.tools.common.model.JavaMethod;
import org.apache.cxf.tools.common.model.JavaModel;
import org.apache.cxf.tools.common.model.JavaParameter;
import org.apache.cxf.tools.wsdlto.frontend.jaxws.generators.SEIGenerator;
import org.apache.cxf.tools.wsdlto.frontend.jaxws.processor.WSDLToJavaProcessor;

public class CustomSEIGenerator extends SEIGenerator {

  private static final String VALID_PARAM  = "VALID_PARAM";

  private static final String VALID_RETURN = "VALID_RETURN";

  @Override
  public void generate(ToolContext penv) throws ToolException {
    JAnnotation validAnno = new JAnnotation(Valid.class);
    Map<QName, JavaModel> map = CastUtils.cast((Map<?, ?>) penv.get(WSDLToJavaProcessor.MODEL_MAP));
    for (JavaModel javaModel : map.values()) {
      Map<String, JavaInterface> interfaces = javaModel.getInterfaces();

      for (JavaInterface intf : interfaces.values()) {
        intf.addImport(Valid.class.getCanonicalName());
        List<JavaMethod> methods = intf.getMethods();

        for (JavaMethod method : methods) {
          List<JavaParameter> parameters = method.getParameters();
          method.addAnnotation(VALID_RETURN, validAnno);
          for (JavaParameter param : parameters) {
            param.addAnnotation(VALID_PARAM, validAnno);
          }
        }
      }
    }

    super.generate(penv);
  }
}

Upvotes: 0

Babl
Babl

Reputation: 7646

Apache CXF's wsdl2java support is pluggable. There is a META-INF/tools-plugin.xml descriptor that allows you to define custom generators ("frontend profiles"). So if you need a @Valid annotation on all cxf-generated webservice interfaces, you can just plug-in a custom SEIGenerator. Apache CXF uses Velocity templates to generate the SEI interfaces. So you just need to overwrite the default template with the custom one.

So instead of using the Anox or Krasa you can just create a simple cxf-codegen-plugin overwrite.

So lets create a separate project, still you can put it in the same project but in different module, but for better reusability I would say a new project.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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">

    <groupId>org.example.test</groupId>
    <artifactId>valid-cxf</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modelVersion>4.0.0</modelVersion>

    <dependencies>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-codegen-plugin</artifactId>
            <version>${org.apache.cxf.version}</version>
        </dependency>
    </dependencies>

</project>

Lets use service loader to define new default SEI generator.

src/main/resources/META-INF/tools-plugin.xml

<?xml version="1.0" encoding="utf-8"?>
<plugin xmlns="http://cxf.apache.org/tools/plugin" name="play" version="" provider="play.typesafe.com">
    <frontend name="sample" package="org.apache.cxf.tools.wsdlto.frontend.jaxws" profile="JAXWSProfile">
    <container name="JAXWSContainer" package="org.apache.cxf.tools.wsdlto.frontend.jaxws" toolspec="jaxws-toolspec.xml"/>
    <processor name="WSDLToJavaProcessor" package="org.apache.cxf.tools.wsdlto.frontend.jaxws.processor"/>
    <builder name="JAXWSDefinitionBuilder" package="org.apache.cxf.tools.wsdlto.frontend.jaxws.wsdl11"/>
    <generators package="com.example.plugin">
        <generator name="CustomSEIGenerator"/>
    </generators>
</frontend>

Here we have defined a new SEIGenerator with name CustomSEIGenerator from the package com.example.plugin

Next lets define a velocity template with all of our overwrites (in our case just hardcoded @Valid annotation on web service interface) This one is based on the official CXF sei.vm

src/main/resources/valid-sei.vm

#if ($intf.packageJavaDoc != "")
/**
$intf.packageJavaDoc
 */
#end
package $intf.PackageName;

#if ($mark-generated == "true")
import javax.annotation.Generated;
#end
import javax.validation.Valid;
#foreach ($import in $intf.Imports)
import ${import};
#end

/**
#if ($intf.classJavaDoc != "")
 $intf.classJavaDoc
 *
 #end
 * This class was generated by $fullversion
 * $currentdate
 * Generated source version: $version
 * 
 */
#foreach ($annotation in $intf.Annotations)
 $annotation
#end
#if ($mark-generated == "true")
@Generated(value = "org.apache.cxf.tools.wsdlto.WSDLToJava", date =     "$currentdate", comments = "$fullversion")
#end
public interface $intf.Name ${sei-superinterface-string}{
#foreach ($method in $intf.Methods)

#if ($method.JavaDoc != "")
    /**
${method.JavaDoc}
     */
#end
#foreach ($annotation in $method.Annotations)
    $annotation
#end
@Valid
#if ($mark-generated == "true")
    @Generated(value = "org.apache.cxf.tools.wsdlto.WSDLToJava", date = "$currentdate")
#end
    public $method.returnValue ${method.Name}(#if($method.ParameterList.size() == 0))#end
#if($method.ParameterList.size() != 0)

#foreach ($param in ${method.ParameterList})
    $param
#end
)#end#if($method.Exceptions.size() > 0) throws#foreach($exception in $method.Exceptions) $exception.ClassName#if($method.Exceptions.size() != $velocityCount),#end#end#end;
#end
}

And at the end lets create the CustomSEIGenerator which will use our velocity template.

package org.example.test;

import org.apache.cxf.tools.common.ToolException;
import org.apache.cxf.tools.wsdlto.frontend.jaxws.generators.SEIGenerator;

import java.io.Writer;

/**
 * Just a sample custom generator which use custom velocity template to generate SEI
 *
 */
public class CustomSEIGenerator extends SEIGenerator {

    @Override
    protected void doWrite(String templateName, Writer outputs) throws ToolException {

        if (templateName.endsWith("/sei.vm")) {
            templateName = "valid-sei.vm";
        }

        super.doWrite(templateName, outputs);
    }
}

Assuming you have build the plugin overwrite and published to your local maven repo, next you just need to add a dependency information to your project pom.xml. So the pom from the question will remain the same only the cxf-codegen-plugin will get a new dependency.

In your pom.xml

<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>
                            </wsdlOption>
                            <extraargs>
                                <extraarg>-fe</extraarg>
                                <extraarg>sample</extraarg>
                            </extraargs>
                        </wsdlOptions>
                    </configuration>
                    <goals>
                        <goal>wsdl2java</goal>
                    </goals>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                <groupId>org.example.test</groupId>                      
                <artifactId>valid-cxf</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    </plugin>

Thats it, now you can fully manage what and how is generated from your wsdl.

A fully working example can be found at this repo https://github.com/babltiga/cxf-valid-sample

Upvotes: 1

Related Questions