Creating a simple SOAP web service with Spring Boot

Simple soap service with Spring Boot

Hi guys! Today we will give another useful topic if you are really worried about web services or APIs (Application Programming Interface). Literally, a service delivered via the web will be treated as a web service. It is designed for machine-to-machine (application-to-application) communication over a network with interoperability. Among the two most popular web service groups, SOAP (Simple Object Access Protocol) is a kind of old-fashioned web service that is still used in application development contexts at the enterprise level. The transport mechanism of SOAP services is either HTTP or MQ but the format of the request and the response should be SOAP XML. These are the key things of SOAP services and now we will look at how we can implement simple SOAP services using the contract-first approach with Spring Boot. That is about a course management system.

SOAP web services with Spring Boot

Initialize with spring initializr

Let’s take a look at the project initialization with https://start.spring.io/

Spring initialzr — SOAP web service
  • Project build tool: Maven
  • Language: Java
  • Spring boot: 2.5.5
  • Project Metadata

Group: com.agnasarp

Artifact: agnasarp-soap-course-management

Name: agnasarp-soap-course-management

Description: Simple SOAP service with Spring Boot

Package name: com.agnasarp.soapcoursemanagement

Packaging: Jar

Java version: 11

  • Dependencies:

Spring Web Services: Facilitates contract-first SOAP development. Allows for the creation of flexible web services using one of the many ways to manipulate XML payloads.

Spring Data JPA: Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate.

H2 Database: Provides a fast in-memory database that supports JDBC API and R2DBC access, with a small (2mb) footprint. Supports embedded and server modes as well as a browser based console application.

Project structure

Project structure of SOAP web service in IntelliJ Idea

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.agnasarp</groupId>
<artifactId>agnasarp-soap-course-management</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>soap-course-management</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws-security</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.sun.xml.wss</groupId>
<artifactId>xws-security</artifactId>
<version>3.0</version>
<exclusions>
<exclusion>
<groupId>javax.xml.crypto</groupId>
<artifactId>xmldsig</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- JAXB2 plugin -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>2.5.0</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- Schema directory -->
<sources>
<source>${project.basedir}/src/main/resources/course-details.xsd</source>
</sources>
<!-- Java class output directory -->
<outputDirectory>${project.basedir}/src/main/java</outputDirectory>
<!-- Package name for generated classes -->
<packageName>com.agnasarp.courses</packageName>
<!-- Package clean -> false -->
<clearOutputDir>false</clearOutputDir>
</configuration>
</plugin>
</plugins>
</build>

</project>

application.properties

Normally, the built-in tomcat is running on port 8080, it may be already assigned for other tasks, so we can change it and the context path of the application as below in the application.properties file

server.port=8180
server.servlet.context-path=/agnasarp-soap-course-management

SoapCourseManagementApplication.java

This is the entry point of the Spring Boot application and now you can run the application on the port 8180.

package com.agnasarp.soapcoursemanagement; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SoapCourseManagementApplication { // This is the entry point of the application. public static void main(String[] args) { SpringApplication.run(SoapCourseManagementApplication.class, args); } }

In the contract-first approach, we have to define all the structures of the requests and responses, so we will define them in the example-files directory as below.

Request.xml

<?xml version="1.0" encoding="UTF-8" ?>
<GetCourseDetailsRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.agnasarp.com/courses"
xsi:schemaLocation="https://www.agnasarp.com/courses course-details.xsd">
<id>123</id>
</GetCourseDetailsRequest>

Response.xml

<?xml version="1.0" encoding="UTF-8" ?>
<GetCourseDetailsResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.agnasarp.com/courses"
xsi:schemaLocation="https://www.agnasarp.com/courses course-details.xsd">
<CourseDetails>
<id>123</id>
<name>SOAP Web Services</name>
<description>SOAP Web Services with Spring Boot</description>
</CourseDetails>
</GetCourseDetailsResponse>

course-details.xsd

This file is to validate all the parameters of SOAP requests and responses. Need to copy and past the same xsd file to the directory /src/main/resources because it should be in the classpath access just by the file name.

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:tns="https://www.agnasarp.com/courses" xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="https://www.agnasarp.com/courses"
elementFormDefault="qualified">
<xs:element name="GetCourseDetailsRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:int"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="GetCourseDetailsResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="CourseDetails" type="tns:CourseDetails"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="GetAllCourseDetailsRequest">
<xs:complexType>
</xs:complexType>
</xs:element>
<xs:element name="GetAllCourseDetailsResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="CourseDetails" type="tns:CourseDetails" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="DeleteCourseDetailsRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:int"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="DeleteCourseDetailsResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="status" type="tns:Status"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="CourseDetails">
<xs:sequence>
<xs:element name="id" type="xs:int"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="description" type="xs:string"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="Status">
<xs:restriction base="xs:string">
<xs:enumeration value="SUCCESS"/>
<xs:enumeration value="FAIL"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

WebServiceConfiguration.java

Now we have to configure all configurations here in the WebServiceConfiguratio.java file. Here we will create few beans for a MessageDispatcherServlet, XSD Schema, DefaultWsdl11Definition as below.

package com.agnasarp.soapcoursemanagement.configuration;


import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.server.EndpointInterceptor;
import org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor;
import org.springframework.ws.soap.security.xwss.callback.SimplePasswordValidationCallbackHandler;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;

import java.util.Collections;
import java.util.List;

//Spring configuration
@Configuration
//Enable Spring Web Service
@EnableWs
public class WebServiceConfiguration extends WsConfigurerAdapter {

// Create a MessageDispatcherServlet and map a URI
@Bean
ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {

MessageDispatcherServlet messageDispatcherServlet = new MessageDispatcherServlet();
messageDispatcherServlet.setApplicationContext(applicationContext);
messageDispatcherServlet.setTransformWsdlLocations(true);

return new ServletRegistrationBean(messageDispatcherServlet, "/ws/*");
}

// Create XSD schema
@Bean
public XsdSchema coursesSchema() {

return new SimpleXsdSchema(new ClassPathResource("course-details.xsd"));
}

// Create a WSDL from the above schema
@Bean(name = "courses")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema coursesSchema) {

DefaultWsdl11Definition defaultWsdl11Definition = new DefaultWsdl11Definition();

defaultWsdl11Definition.setPortTypeName("CoursePort");
defaultWsdl11Definition.setTargetNamespace("https://www.agnasarp.com/courses");
defaultWsdl11Definition.setLocationUri("/ws");
defaultWsdl11Definition.setSchema(coursesSchema);

return defaultWsdl11Definition;
}

//XwsSecurityInterceptor
@Bean
public XwsSecurityInterceptor securityInterceptor() {
XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
//Callback Handler -> SimplePasswordValidationCallbackHandler
securityInterceptor.setCallbackHandler(callbackHandler());
//Security Policy -> securityPolicy.xml
securityInterceptor.setPolicyConfiguration(new ClassPathResource("securityPolicy.xml"));
return securityInterceptor;
}

@Bean
public SimplePasswordValidationCallbackHandler callbackHandler() {
SimplePasswordValidationCallbackHandler handler = new SimplePasswordValidationCallbackHandler();
handler.setUsersMap(Collections.singletonMap("user", "password"));
return handler;
}

//Interceptors.add -> XwsSecurityInterceptor
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(securityInterceptor());
}
}

Course.java

This DTO class is to communicate course details within the application with getters, setters, a constructor, and a toString() method.

package com.agnasarp.soapcoursemanagement.domain;

//DTO for course details
public class Course {

private int id;
private String name;
private String description;

public Course(int id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

@Override
public String toString() {
return "Course{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}

Now we have to define an endpoint for the service that has methods to get the request and return the response. Apart from those methods, there will be few other mapper methods.

CourseDetailsEndpoint.java

package com.agnasarp.soapcoursemanagement.endpoint;

import com.agnasarp.courses.*;
import com.agnasarp.soapcoursemanagement.domain.Course;
import com.agnasarp.soapcoursemanagement.service.CourseDetailsService;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

import java.util.List;

//This marks the class as a SOAP endpoint.
@Endpoint
public class CourseDetailsEndpoint {

CourseDetailsService courseDetailsService;

public CourseDetailsEndpoint(CourseDetailsService courseDetailsService) {
this.courseDetailsService = courseDetailsService;
}

// This method processes the GetCourseDetailsRequest and returns the output.
@PayloadRoot(namespace = "https://www.agnasarp.com/courses", localPart = "GetCourseDetailsRequest")
@ResponsePayload
public GetCourseDetailsResponse processGetCourseDetailsRequest(@RequestPayload GetCourseDetailsRequest getCourseDetailsRequest) {

return courseResponseMapper(courseDetailsService.getCourseById(getCourseDetailsRequest.getId()));
}

// This method processes the GetAllCourseDetailsRequest and returns the output.
@PayloadRoot(namespace = "https://www.agnasarp.com/courses", localPart = "GetAllCourseDetailsRequest")
@ResponsePayload
public GetAllCourseDetailsResponse processGetAllCourseDetailsRequest(@RequestPayload GetAllCourseDetailsRequest getAllCourseDetailsRequest) {

List<Course> courses = courseDetailsService.getAllCourses();
return allCourseResponseMapper(courses);
}

// This method delete a specific course from the course list..
@PayloadRoot(namespace = "https://www.agnasarp.com/courses", localPart = "DeleteCourseDetailsRequest")
@ResponsePayload
public DeleteCourseDetailsResponse processGetAllCourseDetailsRequest(@RequestPayload DeleteCourseDetailsRequest deleteCourseDetailsRequest) {

DeleteCourseDetailsResponse deleteCourseDetailsResponse = new DeleteCourseDetailsResponse();
deleteCourseDetailsResponse.setStatus(mapStatus(courseDetailsService.deleteCourseById(deleteCourseDetailsRequest.getId())));
return deleteCourseDetailsResponse;
}

// Map enum status
private Status mapStatus(CourseDetailsService.Status status) {

if (status == CourseDetailsService.Status.SUCCESS) {
return Status.SUCCESS;
}
return Status.FAIL;
}

// Map course to course details
private CourseDetails courseMapper(Course course) {

CourseDetails courseDetails = new CourseDetails();
courseDetails.setId(course.getId());
courseDetails.setName(course.getName());
courseDetails.setDescription(course.getDescription());

return courseDetails;
}

// Map course details to response
private GetCourseDetailsResponse courseResponseMapper(Course course) {

GetCourseDetailsResponse getCourseDetailsResponse = new GetCourseDetailsResponse();
getCourseDetailsResponse.setCourseDetails(courseMapper(course));
return getCourseDetailsResponse;
}

// Map all course details to response
private GetAllCourseDetailsResponse allCourseResponseMapper(List<Course> courses) {

GetAllCourseDetailsResponse getAllCourseDetailsResponse = new GetAllCourseDetailsResponse();
for (Course course : courses) {
CourseDetails courseDetails = courseMapper(course);
getAllCourseDetailsResponse.getCourseDetails().add(courseDetails);
}

return getAllCourseDetailsResponse;
}
}

Endpoint methods will directly call the CourseDetailsService class to get a course by id, get all courses, and delete a specific course. Here we have some static data to demonstrate the functionality of the service.

CourseDetailsService.java

package com.agnasarp.soapcoursemanagement.service;

import com.agnasarp.soapcoursemanagement.domain.Course;
import com.agnasarp.soapcoursemanagement.exception.CourseNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

//Course details service
@Service
public class CourseDetailsService {

static List<Course> courses = new ArrayList<>();

static {

Course course1 = new Course(1, "Java 11", "Java 11 course");
Course course2 = new Course(2, "Spring 5", "Spring 5 course");
Course course3 = new Course(3, "Spring boot", "Spring boot course");

courses.add(course1);
courses.add(course2);
courses.add(course3);
}

// Get course by id
public Course getCourseById(int id) {

return courses.stream().filter(c -> c.getId() == id).findAny().orElseThrow(() -> new CourseNotFoundException("Invalid course id " + id));
}

// Get all courses
public List<Course> getAllCourses() {

return courses;
}

// Delete a specific course
public Status deleteCourseById(int id) {

Iterator<Course> courseIterator = courses.iterator();
while (courseIterator.hasNext()) {
Course course = courseIterator.next();
if (course.getId() == id) {
courseIterator.remove();
return Status.SUCCESS;
}
}
return Status.FAILS;
}

public enum Status {
SUCCESS, FAILS
}
}

An exception will occur for getCourseById(int id) method if we request a course that is not available in the hardcoded array list. To handle that exception, we can create a custom exception handler class as below.

CourseNotFoundException.java

package com.agnasarp.soapcoursemanagement.exception;

import org.springframework.ws.soap.server.endpoint.annotation.FaultCode;
import org.springframework.ws.soap.server.endpoint.annotation.SoapFault;

@SoapFault(faultCode = FaultCode.CUSTOM, customFaultCode = "{https://www.agnasarp.com/courses}0_COURSE_NOT_FOUND")
public class CourseNotFoundException extends RuntimeException {

/**
* Constructs a new runtime exception with {@code null} as its
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*/
public CourseNotFoundException(String message) {

super(message);
}
}

There will be one final step to apply an authorization mechanism in the application. We had to create few beans in WebServiceConfiguration.java for this and need to add below xml configuration the resources directory.

securityPolicy.xml

<?xml version="1.0" encoding="UTF-8"?>
<xwss:SecurityConfiguration
xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
<xwss:RequireUsernameToken
passwordDigestRequired="false" nonceRequired="false" />
</xwss:SecurityConfiguration>

WSDL file

You can load the WSDL file through the below URL.

http://localhost:8180/agnasarp-soap-course-management/ws/courses.wsdl

WSDL file in the browser

You can use the Wisdler chrome extension to test the SOAP services.

GetCourseDetailsRequest

GetCourseDetails — Request
GetCourseDetails — Response

GetAllCourseDetailsRequest

GetAllCourseDetails — Request
GetAllCourseDetails — Response

DeleteCourseDetailsRequest

DeleteCourseDetails — Request
DeleteCourseDetails — Response

That is it for today. Now you can create SOAP services with Spring Boot very quickly with minimal effort than other services. You can follow along with this post and refer complete GitHub project as in given below. Good luck!

Download source from github:

Download agnasarp-soap-course-management

Originally published at https://www.agnasarp.com on October 12, 2021.

--

--

Agnasarp is a technology-focused blog that has enough information about cutting-edge technologies that you can use for your problems. Stay with us!

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store