Creating SOAP Messages Programmatically with Jakarta EE 11

TL;DR: Learn how to programmatically construct, serialize, and parse SOAP messages using the modern Jakarta SOAP with Attachments (SAAJ 3.0) API. This guide is fully updated for Jakarta EE 11 and WildFly 41.0.1.Final, using Apache CXF as the underlying reference SOAP stack.

In this tutorial, we will learn how to programmatically create a jakarta.xml.soap.SOAPMessage using SOAPBody and SOAPElement objects, and how to parse one from a standard XML String.

First, some architecture: the jakarta.xml.soap.SOAPMessage is the root class for all SOAP messages. As transmitted on the "wire", a SOAP message is an XML document or a MIME message whose first body part is a SOAP XML document. On modern application servers like WildFly 41.0.1.Final, the reference SOAP stack is JBossWS-CXF (built on Apache CXF), which implements these Jakarta EE specifications seamlessly.

SOAP vs. REST: When is SOAP Still Relevant?

While RESTful web services (via Jakarta RESTful Web Services / JAX-RS) have become the default choice for lightweight, web-centric APIs, SOAP remains highly relevant in enterprise integrations. SOAP provides formal contracts via WSDL, built-in standard extensions for enterprise-grade message-level security (WS-Security), and reliable messaging guarantees (WS-ReliableMessaging). When working on secure financial networks, healthcare standard systems, or legacy enterprise middleware integrations, programmatic manipulation of SOAP messages is a vital skill.

Prerequisites & Maven Configuration

To run these examples on a modern Jakarta EE 11 runtime, ensure your project includes the standard Jakarta XML SOAP API dependency. If you are deploying to WildFly 41, this dependency should be marked as provided because the application server already bundles the Apache CXF implementation.

<dependency>
    <groupId>jakarta.xml.soap</groupId>
    <artifactId>jakarta.xml.soap-api</artifactId>
    <version>3.0.1</version>
    <scope>provided</scope>
</dependency>

A SOAPMessage object consists of a SOAP part and optionally one or more attachment parts. The SOAP part for a SOAPMessage object is a SOAPPart object, which contains information used for message routing and identification, and can contain application-specific content. All data in the SOAP Part of a message must be in XML format. The following code shows how to programmatic build a SOAP Request:

import jakarta.xml.soap.MessageFactory;
import jakarta.xml.soap.SOAPMessage;
import jakarta.xml.soap.SOAPPart;
import jakarta.xml.soap.SOAPEnvelope;
import jakarta.xml.soap.SOAPBody;
import jakarta.xml.soap.SOAPElement;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;

private SOAPMessage createSOAPRequest() throws Exception
{
        MessageFactory messageFactory = MessageFactory.newInstance();
        SOAPMessage soapMessage = messageFactory.createMessage();
        SOAPPart soapPart = soapMessage.getSOAPPart();

        // SOAP Envelope
        SOAPEnvelope envelope = soapPart.getEnvelope();
        envelope.addNamespaceDeclaration("acme", "http://samples.saaj.jms");

        // SOAP Body
        SOAPBody soapBody = envelope.getBody();
        SOAPElement soapBodyElem = soapBody.addChildElement("employee", "acme");
        SOAPElement soapBodyElem1 = soapBodyElem.addChildElement("id", "acme");
        soapBodyElem1.addTextNode("10");

        soapMessage.saveChanges();

        System.out.println("Request SOAP Message = ");
        soapMessage.writeTo(System.out);
        System.out.println();
        return soapMessage;
}

This is the structured SOAPMessage XML that will be generated:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:acme="http://samples.saaj.jms">
   <SOAP-ENV:Header />
   <SOAP-ENV:Body>
      <acme:employee>
         <acme:id>10</acme:id>
      </acme:employee>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The following modernized code snippet demonstrates how to safely serialize the SOAPMessage object into a Java String using UTF-8 encoding:

ByteArrayOutputStream outstream = new ByteArrayOutputStream();
soapMessage.writeTo(outstream);
String strMsg = outstream.toString(StandardCharsets.UTF_8);

Create a SOAPMessage from an XML String

Another common strategy is parsing an existing XML String into a programmatic SOAPElement structure. We can utilize a standard SAXParser combined with a custom content handler to map XML nodes directly to SAAJ objects:

package org.example;

import java.io.IOException;
import java.io.StringReader;
import jakarta.xml.soap.SOAPElement;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class CreateSOAPfromXML {

    public static SOAPElement createSOAPElementFromXMLString(String xmlString)
           throws ParserConfigurationException, IOException, SAXException
    {
        StringReader stringReader = new StringReader(xmlString);
        InputSource inputSource = new InputSource(stringReader);
        SAXParserFactory factory = SAXParserFactory.newInstance();
        factory.setNamespaceAware(true);
        SAXParser parser = factory.newSAXParser();
        
        SoapElementHandler handler = new SoapElementHandler();
        parser.parse(inputSource, handler);
        return handler.getSOAPElement();
    }
}

And here is the custom SoapElementHandler implementation required to map the SAX events to our SAAJ object tree:

package org.example;

import java.util.ArrayList;
import java.util.HashMap;
import jakarta.xml.soap.*;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class SoapElementHandler extends DefaultHandler {
	private HashMap<String, String> prefixURIMapping;
	private ArrayList<String> uris;
	private SOAPElement rootElement;
	private SOAPElement currentElement;
	private SOAPFactory soapFactory;

	public SoapElementHandler() {
		prefixURIMapping = new HashMap<>();
		uris = new ArrayList<>();
		rootElement = null;
		currentElement = null;
	}

	public SOAPElement getSOAPElement() {
		return rootElement;
	}

	@Override
	public void startDocument() throws SAXException {
		try {
			soapFactory = SOAPFactory.newInstance();
		} catch (SOAPException e) {
			throw new SAXException("Error: Can't create a SOAPFactory instance", e);
		}
	}

	@Override
	public void startPrefixMapping(String prefix, String uri) {
		prefixURIMapping.put(uri, prefix);
		uris.add(uri);
	}

	@Override
	public void characters(char ch[], int start, int length) throws SAXException {
		String str = String.valueOf(ch);
		if (length > 0) {
			try {
				currentElement.addTextNode(str.substring(start, start + length));
			} catch (SOAPException e) {
				throw new SAXException("Error: Can't add a text node into SOAPElement from text", e);
			}
		}
	}

	@Override
	public void endElement(String uri, String localName, String qName) {
		if (currentElement != rootElement) {
			currentElement = currentElement.getParentElement();
		}
	}

	@Override
	public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
		String prefix = prefixURIMapping.get(namespaceURI);
		try {
			if (rootElement == null && currentElement == null) {
				rootElement = soapFactory.createElement(localName, prefix, namespaceURI);
				currentElement = rootElement;
			} else {
				currentElement = currentElement.addChildElement(localName, prefix, namespaceURI);
			}
			if (uris.size() > 0) {
				for (int i = 0; i < uris.size(); i++) {
					String uri = uris.get(i);
					String pre = prefixURIMapping.get(uri);
					currentElement.addNamespaceDeclaration(pre, uri);
				}
				uris.clear();
			}
			for (int i = 0; i < atts.getLength(); i++) {
				jakarta.xml.soap.Name attriName;
				if (atts.getURI(i) != null && !atts.getURI(i).isEmpty()) {
					String attriPre = prefixURIMapping.get(atts.getURI(i));
					attriName = soapFactory.createName(atts.getLocalName(i), attriPre, atts.getURI(i));
				} else {
					attriName = soapFactory.createName(atts.getLocalName(i));
				}
				currentElement.addAttribute(attriName, atts.getValue(i));
			}

		} catch (SOAPException e) {
			throw new SAXException(e);
		}
	}
}

Using this programmatical framework gives you complete control over SOAP payload elements, allowing seamless integration within custom handlers or direct manipulation before payload delivery inside WildFly 41.


Frequently Asked Questions (FAQs)

1. How do I resolve ClassNotFoundException for SAAJ on WildFly 41?

WildFly 41.0.1.Final bundles Apache CXF as its default SOAP implementation, which already provides the SAAJ standard out-of-the-box. Ensure your Maven dependency for jakarta.xml.soap-api uses the provided scope so that your archive does not deploy duplicate conflicting JARs.

2. What is the main difference between JAX-WS handlers and Apache CXF interceptors?

JAX-WS SOAP handlers (defined by Jakarta EE standards) operate at the application layer on fully realized SOAPMessage models. In contrast, CXF Interceptors are runtime-specific structures that can manipulate the underlying transport streams (incoming/outgoing bytes, raw XML, or SAAJ constructs) at various stages of the phase-based chain, offering much higher performance and flexibility.

3. Is plain HTTP authentication secure for SOAP endpoints?

No, you should never recommend or use Basic Authentication over unencrypted HTTP. Always ensure that your transport layer is protected by HTTPS (TLS) and, when message-level security is required by enterprise guidelines, utilize WS-Security policies to digitally sign or encrypt sensitive XML payload properties.


Recommended Articles

Developing SOAP Web Services on WildFly 41 (Jakarta EE 11) with Apache CXF

Learn how to develop, deploy, and secure Jakarta XML Web Services (SOAP) on WildFly 41.0.1.Final and Jakarta EE 11 using Apache CXF, WS-Security, and modern testing tools.

Deploying SOAP Web Services in Jakarta EE Applications: A Step-by-Step Guide

Learn how to build and deploy SOAP-based web services in Jakarta EE applications, including guidance on the changes in Java SE that removed the JAX-WS API.

How to test SOAP Web Services - WildFly 41 & Jakarta EE 11

Master SOAP Web Services testing under Jakarta EE 11 and WildFly 41. Learn programmatic unit testing, Apache CXF interceptor config, and tool integrations with SoapUI, JMeter, and cURL.

Developing SOAP Web Services with JAX-WS: Code-First Approach

Learn how to develop SOAP web services using the code-first approach and JAX-WS, including Java classes and EJB components.