SOAP WebServices made easily with Spring Boot

2015/06/01

JavaSpringBootSOAP


I know, SOAP-based web services are not cool, but in enterprise world they are still popular since SOAP is well documented and robust and it's been here forever. Recently I have received an assignment to create simple SOAP endpoints. JAVA EE and JAX-WS would be a good choice, but I've decided to stick with Spring WS with Spring Boot as the super-glue.

The assignment covered these topics:

Following the official tutorial, I have started with an empty Maven project adding all the dependencies and preliminary configurations (Spring Boot Application main class, WebService configuration etc.). Next task was creating XSD file with the definition of transfer objects. For brevity, let's assume the definition of transfer object looks like this:

        <xs:complexType name="sampleDto">
            <xs:sequence>
                <xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:int"/>
                <xs:element name="sample_name" type="xs:string"/>
                <xs:element name="description" type="xs:string"/>
                <xs:element minOccurs="0" name="date_added" type="xs:date"/>
            </xs:sequence>
        </xs:complexType>

Next topic was generating classes from the XSD file. For this Maven JAXB2 plugin is the obvious choice. It is easy to setup and configuration options are really wide. Classes are generated using XJC generator - but the first problem that I encountered I already explained in this previous post. XJC needs to have special permissions so that it can work correctly, enough said.

Second problem with generation of classes was the xs:date type, which produces XMLGregorianCalendar-typed fields instead of more widely spread java.util.Date. For this, custom binding and adapter must be created to successfully convert these date types. First of all, a simple DateAdapter class must be made with two methods that will convert the Date to String and vice-versa:

         public class DateAdapter {
         
            public static Date parseDate(String s) {
                return DatatypeConverter.parseDate(s).getTime();
            }
         
            public static String printDate(Date dt) {
                Calendar cal = new GregorianCalendar();
                cal.setTime(dt);
                return DatatypeConverter.printDate(cal).split("\\+")[0];
            }
         
         }

Then, the binding definition file (jaxb-custom-binding.xml) must be created so that the xs:date is properly converted:

          <bindings xmlns="http://java.sun.com/xml/ns/jaxb" version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
            <globalBindings>
                <javaType name="java.util.Date" xmlType="xs:date"
                          parseMethod="com.scor.norda.webservice.util.DateAdapter.parseDate"
                          printMethod="com.scor.norda.webservice.util.DateAdapter.printDate"
                        />
            </globalBindings>
          </bindings>

Ok, close enough, but there is still one thing missing - to be able to use these generated classes for example as DTOs in JPA we need to have all-args constructor present for this class. This can be also setup easily with jaxb2-value-constructor by setting one Maven property xjc.arguments. An excerpt from pom.xml with relevant parts (together with jaxb2 setup) would look like this:

        ...

        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <xjc.arguments>-Xvalue-constructor</xjc.arguments>
        </properties>
        
        ...
        
        <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>jaxb2-maven-plugin</artifactId>
              <version>1.6</version>
              <executions>
                  <execution>
                      <id>xjc</id>
                      <phase>generate-sources</phase>
                      <goals>
                          <goal>xjc</goal>
                      </goals>
                  </execution>
              </executions>
              <configuration>
                  <schemaDirectory>${project.basedir}/src/main/resources/</schemaDirectory>
                  <outputDirectory>${project.basedir}/src/main/jaxb</outputDirectory>
                  <packageName>eu.rapasoft.webservice.dto</packageName>
                  <clearOutputDir>false</clearOutputDir>
                  <!-- TODO: IMPORTANT! You need to execute Maven goal with -Djavax.xml.accessExternalSchema=all VM arguments -->
                  <!-- SEE: http://stackoverflow.com/questions/23011547/webservice-client-generation-error-with-jdk8 -->
                  <bindingDirectory>${project.basedir}/src/main/resources</bindingDirectory>
                  <bindingFiles>jaxb-custom-binding.xml</bindingFiles>
              </configuration>
              <dependencies>
                  <dependency>
                      <groupId>org.jvnet.jaxb2_commons</groupId>
                      <artifactId>jaxb2-value-constructor</artifactId>
                      <version>3.0</version>
                  </dependency>
              </dependencies>
          </plugin>

With all this ready, the generated classes are ready to be used directly as DTOs in JPA queries (e.g. select new eu.rapasoft.dto.SampleDto(...) from ...).

The endpoint itself is really easy. With Spring WS you just create properly annotated class:

        @Endpoint(value = "sample")
        public class SampleEndpoint {
        
            @Autowired
            private SampleRepository sampleRepository;
        
            @PayloadRoot(namespace = NamespaceConstants.NAMESPACE_URI, localPart = "sampleRequest")
            @ResponsePayload
            public SampleResponse getDtos(@RequestPayload SampleRequest sampleRequest) {
                List<SampleDto> legalEntityOmegaMappingDtos = sampleRepository.fetchAll();
        
                return new SampleResponse(legalEntityOmegaMappingDtos);
            }
        
        }

As I wrote in the introduction, next step would be adding simple WS-Security basic authentication. A relevant part of code for this is hidden in Spring Boot configuration class (where also other WS-related settings are defined):

        @EnableWs
        @Configuration
        public class WebServiceConfiguration extends WsConfigurerAdapter {
        
            ...
            // Other WS-related settings
            ...
        
            @Bean
            public XwsSecurityInterceptor securityInterceptor() {
                XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
                securityInterceptor.setCallbackHandler(callbackHandler());
                securityInterceptor.setPolicyConfiguration(new ClassPathResource("securityPolicy.xml"));
                return securityInterceptor;
            }
        
            @Bean
            SimplePasswordValidationCallbackHandler callbackHandler() {
                SimplePasswordValidationCallbackHandler callbackHandler = new SimplePasswordValidationCallbackHandler();
                // TODO @rap: Use real username and passwords
                callbackHandler.setUsersMap(Collections.singletonMap("user", "password"));
                return callbackHandler;
            }
        
            @Override
            public void addInterceptors(List<EndpointInterceptor> interceptors) {
                interceptors.add(securityInterceptor());
            }
            
        }

The securityPolicy.xml file (placed in resources directory) can look like this:

        <xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
        	<xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false" />
        </xwss:SecurityConfiguration>

As you can see, no enhanced encryption, just basic authentication which combined with secure chanel (SSL) should suffice for internal use. The envelope that is being send as a request should contain headers with username/password:

        <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
            <soapenv:Header>
                <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
                    <wsse:UsernameToken>
                        <wsse:Username>user</wsse:Username>
                        <wsse:Password>password</wsse:Password>
                    </wsse:UsernameToken>
                </wsse:Security>
            </soapenv:Header>
            <soapenv:Body>
                ...
            </soapenv:Body>
        </soapenv:Envelope>

And that's basically it. This is just a rough overview, more sense would make viewing the whole source code ;), therefore I have prepared a sample GitHub (without the DB connection and JPA stuff, to decrease complexity a bit).