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;
}
}