boot Posts

Using ClientInterceptor to Modify WSDL String Responses on Spring Boot

Usually we are connecting to a middleware product which are able to provide us a prefect and working webservice request and response. But there are rare cases where response are not perfect, therefore breaking the whole application functionality.

In order to prevent that, sometimes we need to replace and sanitize all the response coming from middleware. But how to do it in Spring Boot?

Actually Spring Boot have an interceptor class for “intercepting” all message that comes, it is called a ClientInterceptor class. We could create a new custom class extending on the ClientInterceptor class to handle all WSDL responses, and do all the modification needed.

So first lets create a simple WSDL for this example,

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions name="Employee"
                  targetNamespace="http://bestpay.payroll/employee" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:tns="http://bestpay.payroll/employee" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <wsdl:types>
        <xsd:schema>
            <xsd:import namespace="http://bestpay.payroll/employee"
                        schemaLocation="employee.xsd" />
        </xsd:schema>
    </wsdl:types>

    <wsdl:message name="employeeLookupRequest">
        <wsdl:part element="tns:EmployeeIdList" name="employeeIdList" />
    </wsdl:message>

    <wsdl:message name="employeeLookupResponse">
        <wsdl:part element="tns:EmployeeInfoList" name="employeeInfoList" />
    </wsdl:message>

    <wsdl:portType name="employeeLookupService">
        <wsdl:operation name="employeeLookup">
            <wsdl:input message="tns:employeeLookupRequest" />
            <wsdl:output message="tns:employeeLookupResponse" />
        </wsdl:operation>
    </wsdl:portType>

    <wsdl:binding name="employeeLookupBinding" type="tns:employeeLookupService">
        <soap:binding style="document"
                      transport="http://schemas.xmlsoap.org/soap/http" />
        <wsdl:operation name="employeeLookup">
            <soap:operation soapAction="http://bestpay.payroll/employee/employeeLookup" />
            <wsdl:input>
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output>
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>

    <wsdl:service name="employeeLookupService">
        <wsdl:port binding="tns:employeeLookupBinding" name="employeeLookupPort">
            <soap:address location="http://localhost" />
        </wsdl:port>
    </wsdl:service>

</wsdl:definitions>

Next is creating a java project by using maven 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">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
        <relativePath/>
    </parent>

    <groupId>com.edw</groupId>
    <artifactId>WSDLReplaceString</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot-bom.version>2.1.6.Final-redhat-00004</spring-boot-bom.version>
        <start-class>com.edw.Main</start-class>
    </properties>

    <dependencyManagement>
        <dependencies>

            <dependency>
                <groupId>me.snowdrop</groupId>
                <artifactId>spring-boot-bom</artifactId>
                <version>${spring-boot-bom.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jvnet.jaxb2.maven2</groupId>
                <artifactId>maven-jaxb2-plugin</artifactId>
                <version>0.14.0</version>
                <executions>
                    <execution>
                        <id>HelloWorld</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <schemaDirectory>${project.basedir}/src/main/resources/wsdl/</schemaDirectory>
                            <schemaIncludes>
                                <schemaInclude>*.wsdl</schemaInclude>
                            </schemaIncludes>
                            <generateDirectory>
                                ${project.build.directory}/generated-sources
                            </generateDirectory>
                            <generatePackage>com.edw.wsdl.bean.hello</generatePackage>
                            <extension>true</extension>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Based on above pom files, we could see that Im using JAXB2 maven plugin to convert WSDL file into Java Object, and using Spring WebService for handling all WS request and response.

Only four java classes involved here, and let me start with the first one. It’s going to be our main class.

package com.edw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class);
    }
}

And a controller class,

package com.edw.controller;

import com.edw.service.ExternalRequestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class MainController {

    @Autowired
    private ExternalRequestService externalRequestService;

    @GetMapping("/")
    public Map index() {
        return new HashMap(){{
            put("response", externalRequestService.getName());
        }};
    }
}

A service class, this is where i put my WSDL endpoint. Basically it would return the employee’s firstname field from WSDL response,

package com.edw.service;

import com.edw.wsdl.bean.hello.EmployeeIdWrapper;
import com.edw.wsdl.bean.hello.EmployeeInfoWrapper;
import com.edw.wsdl.bean.hello.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.client.core.support.WebServiceGatewaySupport;

import javax.xml.bind.JAXBElement;

@Service
public class ExternalRequestService extends WebServiceGatewaySupport {

    @Autowired
    private WebServiceTemplate webServiceTemplate;

    public String getName() {

        EmployeeIdWrapper employeeIdWrapper = new EmployeeIdWrapper();
        employeeIdWrapper.getEid().add("001002");

        JAXBElement<EmployeeInfoWrapper> employeeInfoJaxB = (JAXBElement<EmployeeInfoWrapper>) webServiceTemplate
                .marshalSendAndReceive("http://localhost:8082",
                        new ObjectFactory().createEmployeeIdList(employeeIdWrapper));

        return employeeInfoJaxB.getValue()
                .getEmployeeInfo()
                .get(0)
                .getFirstName();
    }
}

But all of those code wont be running without a configuration class,

package com.edw.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;

@Configuration
public class SOAPConfig {
    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setPackagesToScan("com.edw.wsdl.bean.hello");
        return marshaller;
    }

    @Bean
    public WebServiceTemplate webServiceTemplate() {
        WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.setMarshaller(marshaller());
        webServiceTemplate.setUnmarshaller(marshaller());

        return webServiceTemplate;
    }
}

Im using SoapUI for simulating a middleware product which provide wsdl response,

And try do some rest api call with curl

$ curl -kv http://localhost:8081/
*   Trying ::1:8081...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET / HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.65.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Connection: keep-alive
< Transfer-Encoding: chunked
< Content-Type: application/json
< Date: Wed, 03 Mar 2021 09:53:51 GMT
<
* Connection #0 to host localhost left intact
{"response":"FOO"}                    

As we can see, the response is FOO. Now here comes the scenario where i need to change on-the-fly the response into something else, for this example it would be BAR.

First we need to create a java class to handle all the interception,

package com.edw.config;

import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapVersion;
import org.springframework.ws.soap.saaj.SaajSoapMessage;
import org.springframework.ws.soap.saaj.SaajSoapMessageFactory;

import javax.xml.soap.MessageFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class SOAPClientInterceptor implements ClientInterceptor {

    @Override
    public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
        return true;
    }

    @Override
    public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException {
        return true;
    }

    @Override
    public boolean handleFault(MessageContext messageContext) throws WebServiceClientException {
        return false;
    }

    @Override
    public void afterCompletion(MessageContext messageContext, Exception e) throws WebServiceClientException {
        OutputStream s = null;
        InputStream is1 = null;
        try {
            SaajSoapMessage message = (SaajSoapMessage) messageContext.getResponse();
            s = new ByteArrayOutputStream();
            message.writeTo(s);

            String response = s.toString();
            System.out.println("SOAP RESPONSE: " + response);

            // do all string replacement here
            response = response.replace("FOO", "BAR");

            is1 = new ByteArrayInputStream(response.getBytes());

            SaajSoapMessageFactory saaj = new SaajSoapMessageFactory();
            saaj.setSoapVersion(SoapVersion.SOAP_12);
            saaj.setMessageFactory(MessageFactory.newInstance());

            messageContext.clearResponse();
            messageContext.setResponse(saaj.createWebServiceMessage(is1));

            System.out.println("New Response has been SET");
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if(s != null)
                try {
                    s.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            if(is1 != null)
                try {
                    is1.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
        }
    }
}

And register this newly created class in our SOAPConfig java class,

    @Bean
    public WebServiceTemplate webServiceTemplate() {
        WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.setMarshaller(marshaller());
        webServiceTemplate.setUnmarshaller(marshaller());

        // register our interceptor here
        webServiceTemplate.setInterceptors(new ClientInterceptor[]{new SOAPClientInterceptor()});

        return webServiceTemplate;
    }

And we can try to do a rest api call with curl to see whether response has changed or not,

$ curl -kv http://localhost:8081/
*   Trying ::1:8081...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET / HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.65.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Connection: keep-alive
< Transfer-Encoding: chunked
< Content-Type: application/json
< Date: Wed, 03 Mar 2021 10:15:49 GMT
<
* Connection #0 to host localhost left intact
{"response":"BAR"}                                      

As we can see, our previous FOO has been changed into BAR which shows that finally we can replace WS response on-the-fly.

Code can be found here,

https://github.com/edwin/spring-boot-ws-response-modification

Deploying Spring Boot with A Dynamic application.properties Location to Openshift

I want to create a simple spring boot app, and deploy it to Openshift 4.2. It supposed to be a straigh forward task, but the problem is that it is required to externalize all configuration to a configmaps or secret so no need to recompile the whole app in case of configuration change.

There are several approach of externalizing configuraton to configmaps, one way is put it as a string literal, include on your pod and call on application via environment variables, or deploy the whole configuration file and mount it on your Openshift pod. The last approach is the one that we will be doing now today.

First lets start with deploying our properties to Openshift as configmaps,

oc create cm myapp-api-configmap --from-file=D:\source\my-app\src\main\resources\application.properties

We can check and validate the result,

oc get cm

oc describe cm myapp-api-configmap

After that, we can mount corresponding configmap to a specific folder on our Pod, on below example modification is done on DeploymentConfig.yaml and mounting application.properties to /deployments/config folder.

kind: DeploymentConfig
apiVersion: apps.openshift.io/v1
metadata:
  ........
    spec:
      volumes:
        - name: myapp-api-configmap-volume
          configMap:
            name: myapp-api-configmap
            defaultMode: 420
      containers:
        - name: myapp-api
          image: >-
            image-registry.openshift-image-registry.svc:5000/openshift/myapp@sha256:1127.....
          ports:
            - containerPort: 8778
              protocol: TCP
            - containerPort: 8080
              protocol: TCP
            - containerPort: 8443
              protocol: TCP
          resources: {}
          volumeMounts:
            - name: myapp-api-configmap-volume
              mountPath: /deployments/config
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          imagePullPolicy: Always
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
      dnsPolicy: ClusterFirst
      securityContext: {}
      schedulerName: default-scheduler

A modification is also needed on my Dockerfile, pointing a new path for my properties file by using “spring.config.location” parameter,

FROM registry.access.redhat.com/openjdk/openjdk-11-rhel7

USER jboss
RUN mkdir -p /deployments/image && chown -R jboss:0 /deployments
EXPOSE 8080

COPY target/application-1.0.jar /deployments/application.jar
CMD ["java", "-jar", "/deployments/application.jar", "--spring.config.location=file:///deployments/config/application.properties"]

Build, deploy,and see that application is now taking configuration from external configuration file.