JAX-WS client with Maven

Introduction

This article will build, step by step, a full SOAP WS client, starting from scratch with just a WSDL file:

+First, we'll create a maven SOAP wrapper maven project that, in a completely automatically way, will create all the necessary Java classes to interact with the WS.

+Then, we'll create another maven project, which will use the wrapper one as a dependency, that will implement the WS client logic.

-In addition, this WS client will implement a JAX-WS handler and will allow to intercept and log all remote SOAP requests, responses and faults.


Import: jakarta vs javax (introduction)

Spring Boot >= 3 requires the jakarta ones:

jakarta.xml.ws.BindingProvider        javax.xml.ws.BindingProvider

1 Reference

JAX-WS documentation and reference implementation (RI) > https://jax-ws.java.net/

Maven software project management and comprehension tool > https://maven.apache.org/2


3 The SOAP WS wrapper maven project

This project is the final working SOAP WS wrapper project described in this section.

-Place the WSDL file, "zuora.a.80.0.wsdl", in the proper maven directory

src\main\resources\wsdls\

-Customize the JAXB binding file, "zuora_wsdl_bindings.xjb", that will be passed to the JAXB compiler. In the directory:

src\main\resources\jaxws\

<?xml version="1.0" encoding="UTF-8"?>

<bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"

        xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"

        xmlns:xsd="http://www.w3.org/2001/XMLSchema"

        xmlns="http://java.sun.com/xml/ns/jaxws">

    <bindings node="//xsd:schema[@targetNamespace='http://object.api.zuora.com/']">

        <jaxb:globalBindings generateElementProperty="false" underscoreBinding="asCharInWord"/>

    </bindings>

</bindings>

-Customize the JAX-WS catalog using file "jax-ws-catalog.xml" to avoid connection and download of the WSDL at runtime. In directory:

src\main\resources\META-INF\

<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog" prefer="system">

        <system systemId="http://fake.url/wsdls/SOAService.wsdl" uri="../wsdls/zuora.a.80.0.wsdl"/>

</catalog>

-Let's glue all pieces together at the maven project "pom.xml" file:

At the <project><build><plugins> section, let's add the 'plugin' for the artifactId "jaxws-tools" of the groupId "org.codehaus.mojo".

The maven goal we are interested in is "wsimpot" which actually parses the WSDL file and generates the corresponding Java classes.

Notice that depending on WSDL structure, you might need to configure additional flags as -XautoNameResolution or -XadditionalHeaders.

<configuration>

    <xjcArgs>

        <!-- Enable "-B-XautoNameResolution" ONLY if needed -->

        <xjcArg>-XautoNameResolution</xjcArg>

    </xjcArgs>

    <args>

        <!-- args as explained at http://blog.udby.com/archives/132 -->

        <!-- Enable "-XadditionalHeaders" ONLY if needed -->

        <arg>-XadditionalHeaders</arg>

    </args>

    <!-- wsdls file directory -->

    <wsdlDirectory>${basedir}/src/main/resources/wsdls</wsdlDirectory>

    <!-- which wsdl file -->

    <wsdlFiles>

        <wsdlFile>zuora.a.80.0.wsdl</wsdlFile>

    </wsdlFiles>

    <!--

         Using file 'jax-ws-catalog.xml' to load wsdl from local path (for JUnit purposes only):

         http://stackoverflow.com/questions/4163586/jax-ws-client-whats-the-correct-path-to-access-the-local-wsdl

    -->

    <wsdlLocation>http://fake.url/wsdls/SOAService.wsdl</wsdlLocation>                           

    <catalog>${basedir}/src/main/resources/META-INF/jax-ws-catalog.xml</catalog>

    <bindingDirectory>${basedir}/src/main/resources/jaxws</bindingDirectory>

    <bindingFiles>

        <!-- Enable "bindingFile" ONLY if needed -->

        <bindingFile>zuora_wsdl_bindings.xjb</bindingFile>

    </bindingFiles>

    <!-- Keep generated files -->

    <keep>true</keep>

    <!-- generated source files destination -->

    <sourceDestDir>${project.build.directory}/generated-sources/jaxws</sourceDestDir>

    <verbose>true</verbose>

    <!--  target. generated code as per jax-ws specification (2.2 is the default one for JEE 7) -->

    <target>2.2</target>

</configuration>

4 The SOAP WS client maven project

This maven WS client project must have the previous wrapper one as a dependency and it has the following top features:

-It overrides the WSDL file WS endpoint URL

-It's able to log all SOAP requests, responses and faults

-It overrides the default WS connection client timeout

-It overrides the default WS response client timeout

The following piece of code shows a class with two methods to login into the remote system described by the WSDL file and one for getting information about the user logged-in:

package com.axaa.cross.zuora;

import com.zuora.api.UnexpectedErrorFault;

import com.zuora.api.ZuoraService;

import java.util.ArrayList;

import java.util.List;

import java.util.Map;

import java.util.logging.Level;

import java.util.logging.Logger;

import javax.xml.ws.BindingProvider;

import javax.xml.ws.Holder;

import com.zuora.api.LoginFault;

import com.zuora.api.LoginResult;

import com.zuora.api.SessionHeader;

import com.zuora.api.Soap;

public class ZuoraBusiness{

    // Only info, warning and severe log to console by java.util.logging.Logger

    private static final Logger LOGGER = Logger.getLogger(ZuoraBusiness.class.getName());

    // No really able to justify this number, but 17" seams much more than big enough for connection

    private static final int WS_CONNECT_TIMEOUT = 1024;

    // Zuora max server side timeout is 120" + adding some extra time for transportation

    private static final int WS_REQUEST_TIMEOUT = 120000 + 200;

    // Proxy used by a service client for invoking operations on the target service point (see method get*Port for type)

    private Soap port;

    //sessionHeader initialized by a successful login operation, it's needed for any subsequent invocation to any another Zuora remote operation

    private SessionHeader sessionHeader;

    /**

     * Constructor

     *

     * @param endPointUrl

     *            Zuora endpoint requested (version must match the WSDL one, since

     *            dependency 'zuora-soap-wrapper' uses a fixed version of the

     *            API), eg: https://apisandbox.zuora.com/apps/services/a/80.0

     */

    public ZuoraBusiness(final String endPointUrl) {

        super();

        this.setPort(new ZuoraService(), endPointUrl);

    }

    /**

     * Login operation

     * @param username

     * @param password

     * @param entityId Ignored, multi-entity was not enabled at time of writing

     * @param entityName Ignored, multi-entity was not enabled at time of writing

     * @return

     * @throws

     */

    public LoginResult login(final String username, final String password, final String entityId,

            final String entityName) throws LoginFault, UnexpectedErrorFault {

        //

        LoginResult ret;

        ret = this.port.login(username, password);

        this.setSessionHeader(ret.getSession());

        //

        return ret;

    }

    /**

     * Login operation

     *

     * @param username

     * @param password

     * @return

     * @throws

     */

    public LoginResult login(final String username, final String password) throws LoginFault, UnexpectedErrorFault{

        return this.login(username, password, (String) null, (String) null);

    }

    /**

     *

     * @param tenantId OUT argument (Holder just acts as a wrapper for the String immutable type)

     * @param tenantName OUT argument (Holder just acts as a wrapper for the String immutable type)

     * @param userEmail OUT argument (Holder just acts as a wrapper for the String immutable type)

     * @param userFullName OUT argument (Holder just acts as a wrapper for the String immutable type)

     * @param userId OUT argument (Holder just acts as a wrapper for the String immutable type)

     * @param userName OUT argument (Holder just acts as a wrapper for the String immutable type)

     * @throws UnexpectedErrorFault -

     */

public void getUserInfo(final Holder<String> tenantId, final Holder<String> tenantName, final Holder<String> userEmail, final Holder<String> userFullName, final Holder<String> userId, final Holder<String> userName) throws UnexpectedErrorFault{

    //Invoke Zuora WS operation providing the 'sessionHeader' received upon successful login

    this.port.getUserInfo(tenantId, tenantName, userEmail, userFullName, userId, userName, this.sessionHeader);

}

   

    /*- */

    private void setSessionHeader(final String sessionId) {

        this.sessionHeader = new com.zuora.api.ObjectFactory().createSessionHeader();

        this.sessionHeader.setSession(sessionId);

    }

    /*- Cannot change Zuora API version, since 'zoura-soap-wrapper' dependency is linked to exactly one WSDL version.

     *

     *

     */

    @SuppressWarnings("rawtypes")

    private void setPort(final ZuoraService service, final String endPointUrl) {

        this.port = service.getSoap();

        BindingProvider bindingProvider = (BindingProvider) this.port;

        //

        Map<String, Object> requestContext = bindingProvider.getRequestContext();

        requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endPointUrl);

        requestContext.put(com.sun.xml.ws.client.BindingProviderProperties.CONNECT_TIMEOUT, WS_CONNECT_TIMEOUT);

        requestContext.put(com.sun.xml.ws.client.BindingProviderProperties.REQUEST_TIMEOUT, WS_REQUEST_TIMEOUT);

        //

        if (LOGGER.isLoggable(Level.CONFIG)) {

            LOGGER.config("Connect timeout (ms)=" + WS_CONNECT_TIMEOUT + "; Request timeout (ms)=" + WS_REQUEST_TIMEOUT

                    + "; Enpoint=" + endpointUr);

        }

        // INI Add a jax-ws logging handler

        javax.xml.ws.Binding binding = bindingProvider.getBinding();

        List<javax.xml.ws.handler.Handler> handlerList = binding.getHandlerChain();

        if (handlerList == null) {

            handlerList = new ArrayList<javax.xml.ws.handler.Handler>();

        }

        handlerList.add(new MyJAXWSLoggingHandler(LOGGER));

        binding.setHandlerChain(handlerList);

        // END Add a jax-ws logging handler

    }

   

   

}

4.1 The JAX-WS logging handler (interceptor)

This is the implementation of the class "MyJAXWSLoggingHandler" used by the client WS implementation to log SOAP requests, responses and faults:

package com.axaa.cross.zuora;

import java.io.ByteArrayOutputStream;

import java.io.IOException;

import java.util.Set;

import java.util.logging.Level;

import java.util.logging.Logger;

import javax.xml.namespace.QName;

import javax.xml.soap.SOAPException;

import javax.xml.soap.SOAPMessage;

import javax.xml.ws.handler.MessageContext;

import javax.xml.ws.handler.soap.SOAPHandler;

import javax.xml.ws.handler.soap.SOAPMessageContext;

/**

 * JAX-WS handler (not exactly the same than interceptor)

 * <br>

 * Note: It masks passwords (they are never logged)<br>

 * It masks sessionIds (they are never logged)<br>

 *

 * @see <a href="https://jax-ws.java.net/articles/handlers_introduction.html">JAX-WS handlers</a>

 * @author <a href="mailto:jm.rodriguez@ibermatica.com">Jose M. Rodriguez</a>

 */

public class MyJAXWSLoggingHandler implements SOAPHandler<SOAPMessageContext> {

    private final Logger logger; //info(), warning() and severe() automatically log to console

    /**

     * Constructor. A minimum of Level.FINE for java.util.logging.logger need

     * for logging SOAP requests/responses

     *

     * @param logger

     *            java.util.logging.Logger to log SOAP requests, responses and faults

     */

    public MyJAXWSLoggingHandler(final Logger logger) {

        super();

        this.logger = logger;

    }

    @Override

    public boolean handleMessage(SOAPMessageContext c) {

 

        if (this.logger.isLoggable(Level.FINE)) {

            final SOAPMessage msgSoap = c.getMessage();

            boolean request = ((Boolean) c.get(SOAPMessageContext.MESSAGE_OUTBOUND_PROPERTY)).booleanValue();

            try {

                final String msgMasked = msgMask(toString(msgSoap));

                if (request) { // This is a request message.

                    this.logger.fine("----SOAP request message----\n" + msgMasked + "\n\n");

             

                } else { // This is the response message

                    this.logger.fine("----SOAP response message----\n" + msgMasked + "\n\n");

                   

                }

            } catch (Throwable e) {

                System.err.println(e.getMessage());

            }

        }

        //

        return true;

    }

    @Override

    public boolean handleFault(SOAPMessageContext c) {

        if (this.logger.isLoggable(Level.SEVERE)) {

            final SOAPMessage msg = c.getMessage();

            try {

               this.logger.severe("\n\n----SOAP fault message----\n" + toString(msg));

            } catch (Throwable e) {

                System.err.println("Caught throwable handleFault : " + e.getMessage());

            }

        }

        //

        return true;

    }

    @Override

    public void close(MessageContext context) {

        if (this.logger.isLoggable(Level.FINE)) {

            this.logger.fine("\n\n----SOAP close----");

        }

    }

    @Override

    public Set<QName> getHeaders() {

        // Not required for logging

        return null;

    }

    /*- It masks SOAP message content not intended to be logged:

     * ·the password

     * ·the sessionId (both tags uppercase and lowercase)

     * */

    private static String msgMask(final String soapMsg) {

        // Password (as intercepted by this handler)

        final String passwordRegex = "<password>(.+?)</password>";

        final String passwordReplacement = "<password>Scrapped by #cross JAX-WS logging handler</password>";

        // Session(uppercase) in LoginResult (SoapUI):

        // <ns1:Session>Q7nSbg7aa9mm3K2EUNiS52gvKV...qTQSzWPl_ORjwIHFKKUSfCU=</ns1:Session>

        final String session1Regex = "Session>(.+?)<";

        final String session1Replacement = "Session>#cross handler scrapped<";

        // Session(lowercase) in request header (SoapUI):

        // <api:session>Q7nSbg7aa9mm3K2EUNiS52gvKV...qTQSzWPl_ORjwIHFKKUSfCU=</api:session>

        final String session2Regex = "session>(.+?)<";

        final String session2Replacement = "Session>#cross session scrapped<";

        //

        return soapMsg.replaceAll(passwordRegex, passwordReplacement).replaceAll(session1Regex, session1Replacement)

                .replaceAll(session2Regex, session2Replacement);

    }

    /*- It converts a SOAPMesage to a String */

    private static String toString(final SOAPMessage msg) throws SOAPException, IOException {

        final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        msg.writeTo(baos);

        String charsetEncoding = (String) msg.getProperty(SOAPMessage.CHARACTER_SET_ENCODING);

        if (charsetEncoding == null) {

            charsetEncoding = java.nio.charset.StandardCharsets.UTF_8.name();

        }

        String ret = baos.toString();

        //

        return ret;

    }

}