Listing 3. Using SAAJ to access the Roman Numerals Conversion Web service
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.Iterator;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPConnection;
import javax.xml.soap.SOAPConnectionFactory;
import javax.xml.soap.SOAPConstants;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
public class RomanNumerals extends JFrame
{
private JTextField txtResult;
RomanNumerals()
{
super("RomanNumerals");
setDefaultCloseOperation(EXIT_ON_CLOSE);
// Create a gradient panel in which to present the GUI.
GPanel pnl = new GPanel();
pnl.setLayout(new BorderLayout());
// Build input panel.
JPanel pnlInput = new JPanel();
Border inner = BorderFactory.createEtchedBorder();
Border outer = BorderFactory.createEmptyBorder(10, 10, 10, 10);
pnlInput.setBorder(BorderFactory.createCompoundBorder(outer, inner));
pnlInput.setOpaque(false);
pnlInput.add(new JLabel("Enter Roman numerals or integer:"));
final JTextField txtInput = new JTextField(15);
pnlInput.add(txtInput);
pnl.add(pnlInput, BorderLayout.NORTH);
// Build buttons panel.
JPanel pnlButtons = new JPanel();
inner = BorderFactory.createEtchedBorder();
outer = BorderFactory.createEmptyBorder(10, 10, 10, 10);
pnlButtons.setBorder(BorderFactory.createCompoundBorder(outer, inner));
pnlButtons.setOpaque(false);
JButton btnToRoman = new JButton("To Roman");
ActionListener alToRoman;
alToRoman = (ae) ->
{
try
{
String roman = toRoman(txtInput.getText());
txtResult.setText(roman);
}
catch (SOAPException se)
{
JOptionPane.showMessageDialog(RomanNumerals.this,
se.getMessage());
}
};
btnToRoman.addActionListener(alToRoman);
pnlButtons.add(btnToRoman);
JButton btnToInteger = new JButton("To Integer");
ActionListener alToInteger;
alToInteger = (ae) ->
{
try
{
String integer = toInteger(txtInput.getText());
txtResult.setText(integer);
}
catch (SOAPException se)
{
JOptionPane.showMessageDialog(RomanNumerals.this,
se.getMessage());
}
};
btnToInteger.addActionListener(alToInteger);
pnlButtons.add(btnToInteger);
pnl.add(pnlButtons, BorderLayout.CENTER);
// Build result panel.
JPanel pnlResult = new JPanel();
inner = BorderFactory.createEtchedBorder();
outer = BorderFactory.createEmptyBorder(10, 10, 10, 10);
pnlResult.setBorder(BorderFactory.createCompoundBorder(outer, inner));
pnlResult.setOpaque(false);
pnlResult.add(new JLabel("Result:"));
txtResult = new JTextField(35);
pnlResult.add(txtResult);
pnl.add(pnlResult, BorderLayout.SOUTH);
setContentPane(pnl);
pack();
setResizable(false);
setLocationRelativeTo(null); // center on the screen
setVisible(true);
}
String toInteger(String input) throws SOAPException
{
// Build a request message. The first step is to create an empty message
// via a message factory. The default SOAP 1.1 message factory is used.
MessageFactory mfactory = MessageFactory.newInstance();
SOAPMessage request = mfactory.createMessage();
// The request SOAPMessage object contains a SOAPPart object, which
// contains a SOAPEnvelope object, which contains an empty SOAPHeader
// object followed by an empty SOAPBody object.
// Detach the header since a header is not required. This step is
// optional.
SOAPHeader header = request.getSOAPHeader();
header.detachNode();
// Access the body so that content can be added.
SOAPBody body = request.getSOAPBody();
// Add the RomanToInt operation body element to the body.
QName bodyName = new QName("http://javajeff.ca/", "RomanToInt", "tns");
SOAPBodyElement bodyElement = body.addBodyElement(bodyName);
// Add the Rom child element to the RomanToInt body element.
QName name = new QName("Rom");
SOAPElement element = bodyElement.addChildElement(name);
element.addTextNode(input).setAttribute("xsi:type", "xs:string");
// Add appropriate namespaces and an encoding style to the envelope.
SOAPEnvelope env = request.getSOAPPart().getEnvelope();
env.addNamespaceDeclaration("env",
"http://schemas.xmlsoap.org/soap/envelop/");
env.addNamespaceDeclaration("enc",
"http://schemas.xmlsoap.org/soap/encoding/");
env.setEncodingStyle(SOAPConstants.URI_NS_SOAP_ENCODING);
env.addNamespaceDeclaration("xs", "http://www.w3.org/2001/XMLSchema");
env.addNamespaceDeclaration("xsi",
"http://www.w3.org/2001/XMLSchema-instance");
// Output the request just built to standard output, to see what the
// SOAP message looks like (which is useful for debugging).
System.out.println("\nSoap request:\n");
try
{
request.writeTo(System.out);
}
catch (IOException ioe)
{
JOptionPane.showMessageDialog(RomanNumerals.this,
ioe.getMessage());
}
System.out.println();
// Prepare to send message by obtaining a connection factory and creating
// a connection.
SOAPConnectionFactory factory = SOAPConnectionFactory.newInstance();
SOAPConnection con = factory.createConnection();
// Identify the message's target.
String endpoint = "http://www.javajeff.ca/php/rncws.php";
// Call the Web service at the target using the request message. Capture
// the response message and send it to standard output.
SOAPMessage response = con.call(request, endpoint);
System.out.println("\nSoap response:\n");
try
{
response.writeTo(System.out);
}
catch (IOException ioe)
{
JOptionPane.showMessageDialog(RomanNumerals.this,
ioe.getMessage());
}
// Close the connection to release resources.
con.close();
// Return a response consisting of the reason for a SOAP Fault or the
// value of the RomanToIntResponse body element's return child element.
if (response.getSOAPBody().hasFault())
return response.getSOAPBody().getFault().getFaultString();
else
{
body = response.getSOAPBody();
bodyName = new QName("urn:Roman-IRoman", "RomanToIntResponse", "NS1");
Iterator iter = body.getChildElements(bodyName);
bodyElement = (SOAPBodyElement) iter.next();
iter = bodyElement.getChildElements(new QName("return"));
return ((SOAPElement) iter.next()).getValue();
}
}
String toRoman(String input) throws SOAPException
{
// Build a request message. The first step is to create an empty message
// via a message factory. The default SOAP 1.1 message factory is used.
MessageFactory mfactory = MessageFactory.newInstance();
SOAPMessage request = mfactory.createMessage();
// The request SOAPMessage object contains a SOAPPart object, which
// contains a SOAPEnvelope object, which contains an empty SOAPHeader
// object followed by an empty SOAPBody object.
// Detach the header since a header is not required. This step is
// optional.
SOAPHeader header = request.getSOAPHeader();
header.detachNode();
// Access the body so that content can be added.
SOAPBody body = request.getSOAPBody();
// Add the IntToRoman operation body element to the body.
QName bodyName = new QName("http://javajeff.ca/", "IntToRoman", "tns");
SOAPBodyElement bodyElement = body.addBodyElement(bodyName);
// Add the Int child element to the IntToRoman body element.
QName name = new QName("Int");
SOAPElement element = bodyElement.addChildElement(name);
element.addTextNode(input).setAttribute("xsi:type", "xs:int");
// Add appropriate namespaces and an encoding style to the envelope.
SOAPEnvelope env = request.getSOAPPart().getEnvelope();
env.addNamespaceDeclaration("env",
"http://schemas.xmlsoap.org/soap/envelop/");
env.addNamespaceDeclaration("enc",
"http://schemas.xmlsoap.org/soap/encoding/");
env.setEncodingStyle(SOAPConstants.URI_NS_SOAP_ENCODING);
env.addNamespaceDeclaration("xs", "http://www.w3.org/2001/XMLSchema");
env.addNamespaceDeclaration("xsi",
"http://www.w3.org/2001/XMLSchema-instance");
// Output the request just built to standard output, to see what the
// SOAP message looks like (which is useful for debugging).
System.out.println("\nSoap request:\n");
try
{
request.writeTo(System.out);
}
catch (IOException ioe)
{
JOptionPane.showMessageDialog(RomanNumerals.this,
ioe.getMessage());
}
System.out.println();
// Prepare to send message by obtaining a connection factory and creating
// a connection.
SOAPConnectionFactory factory = SOAPConnectionFactory.newInstance();
SOAPConnection con = factory.createConnection();
// Identify the message's target.
String endpoint = "http://www.javajeff.ca/php/rncws.php";
// Call the Web service at the target using the request message. Capture
// the response message and send it to standard output.
SOAPMessage response = con.call(request, endpoint);
System.out.println("\nSoap response:\n");
try
{
response.writeTo(System.out);
}
catch (IOException ioe)
{
JOptionPane.showMessageDialog(RomanNumerals.this,
ioe.getMessage());
}
// Close the connection to release resources.
con.close();
// Return a response consisting of the reason for a SOAP Fault or the
// value of the IntToRomanResponse body element's return child element.
if (response.getSOAPBody().hasFault())
return response.getSOAPBody().getFault().getFaultString();
else
{
body = response.getSOAPBody();
bodyName = new QName("urn:Roman-IRoman", "IntToRomanResponse", "NS1");
Iterator iter = body.getChildElements(bodyName);
bodyElement = (SOAPBodyElement) iter.next();
iter = bodyElement.getChildElements(new QName("return"));
return ((SOAPElement) iter.next()).getValue();
}
}
public static void main(String[] args)
{
EventQueue.invokeLater(() -> new RomanNumerals());
}
}
class GPanel extends JPanel
{
private GradientPaint gp;
@Override
public void paintComponent(Graphics g)
{
if (gp == null)
gp = new GradientPaint(0, 0, Color.pink, 0, getHeight(), Color.orange);
// Paint a nice gradient background with pink at the top and orange at
// the bottom.
((Graphics2D) g).setPaint(gp);
g.fillRect(0, 0, getWidth(), getHeight());
}
}
Listing 3 combines Swing/Abstract Window Toolkit code for creating a user interface with SAAJ code for communicating with the Roman Numerals Conversion Web service.
The user interface consists of a pair of text fields and a pair of buttons. One of these text fields is used to enter the Roman numerals or base-10 integer digits of the value to be converted. The other text field displays the conversion result. Click one of the buttons to convert from Roman numerals to integer digits; click the other button to achieve the opposite conversion. In response to a button click, either the String toInteger(String input)
method or the String toRoman(String input)
method is called to perform the conversion.
Consider the GPanel
(Gradient Panel) class. I introduced GPanel
so that I could generate a colorful background for the application's window.
GPanel
extends javax.swing.JPanel
to describe a custom panel whose surface is painted with a gradient whenever its inherited void paintComponent(Graphics g)
method is called. This happens when the window is first displayed, and when the window is restored after being minimized (at least on Windows platforms).
GPanel
uses the java.awt.GradientPaint
class to paint the gradient. (I could have used the java.awt.LinearGradientPaint
class instead, but flipped a coin and ended up using GradientPaint
.) The first two arguments passed to this class's constructor identify the upper-left corner of the rectangular area over which the gradient is drawn, the third argument specifies the color at the top of the gradient, the fourth and fifth arguments identify the rectangular area's lower-right corner, and the final argument identifies the color at the bottom of the gradient.
Ideally, the user interface's components appear over a gradient background, and not over some intermediate background. However, because the user interface is created from panels of components added to the gradient panel, the gradient panel's surface will not show through these "upper" panels unless they are made transparent, by calling their void setOpaque(boolean opaque)
method with false
as the argument. For example, pnlInput.setOpaque(false);
makes the input panel (the panel containing a label and input text field) transparent so that the gradient background shows through.
Listing 3 uses SOAPMessage
's void writeTo(OutputStream out)
method to output a request or response message to the standard output stream. You'll find this feature helpful for understanding the relationship between SAAJ API calls and the SOAP messages that are constructed, especially if you're having difficulty following the API calls. This feature is also helpful when you've created a SOAP-based Web service with a Service Endpoint Interface (SEI) and Service Implementation Bean (SIB) and are trying to create a SAAJ-based client.
Compiling and running the roman numerals application
Compile Listing 3 as follows:
javac --add-modules java.xml.ws RomanNumerals.java
You can omit --add-modules java.xml.ws
when compiling under Java SE 8.
Run the resulting application as follows:
java --add-modules java.xml.ws RomanNumerals
Again, you can omit --add-modules java.xml.ws
when running under Java SE 8.
Figure 2 shows the resulting window with an example conversion from 2017 to MMXVII.
Figure 2. Converting 2017 to its Roman Numerals counterpart
Additionally RomanNumerals
outputs the following request and response SOAP messages (reformatted for readability):
Soap request:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:enc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:env="http://schemas.xmlsoap.org/soap/envelop/"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<tns:IntToRoman xmlns:tns="http://javajeff.ca/">
<Int xsi:type="xs:int">2017</Int>
</tns:IntToRoman>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Soap response:
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="urn:Roman-IRoman"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:IntToRomanResponse>
<return xsi:type="xsd:string">MMXVII</return>
</ns1:IntToRomanResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Extracting response values
Note the response MMXVII
value. Each of Listing 3's toInteger()
and toRoman()
methods extracts the response value by first checking the response message's body to learn if it describes a fault. This task is accomplished by invoking SOAPBody
's boolean hasFault()
method.
If hasFault()
returns true
, SOAPBody
's SOAPFault getFault()
method is called. This method returns an object that describes the fault in terms of the SOAPFault
interface's methods, and SOAPFault
's String getFaultString()
method is called to return the string-based fault message.
If hasFault()
returns false
, the message's body provides the response value that must be extracted. The following excerpt from the toRoman()
method handles this extraction task:
body = response.getSOAPBody();
bodyName = new QName("urn:Roman-IRoman", "IntToRomanResponse", "NS1");
Iterator iter = body.getChildElements(bodyName);
bodyElement = (SOAPBodyElement) iter.next();
iter = bodyElement.getChildElements(new QName("return"));
return ((SOAPElement) iter.next()).getValue();
After calling SOAPMessage
's SOAPBody getSOAPBody()
convenience method to return the SOAPBody
object describing the SOAP message's body, the excerpt creates a QName
object that identifies the qualified name for the IntToRomanResponse
element. This object is then passed to SOAPBody
's inherited Iterator getChildElements(QName qname)
method to return a java.util.Iterator
instance that will be used to iterate over all IntToRomanResponse
child elements of the Body
element.
Because there's only one such child element, only a single call to next()
is made to return this element, as a SOAPBodyElement
instance. This object is used to invoke getChildElements()
, but this time with the qualified name of the return
element. The returned iterator's next()
method is called to extract the return
element as a SOAPElement
instance, and getValue()
is invoked on this instance to return the value of the return
element, which happens to be MMXVII
.
Logging SOAP messages with a JAX-WS handler
The RomanNumerals
application used SOAPMessage
's void writeTo(OutputStream out)
method to dump SOAP messages to the standard output stream. If you want to accomplish this task in the context of Part 2's UCClient
application, you need to install a JAX-WS handler.
JAX-WS lets you install a chain of handlers on a Web service class, a client class, or both to perform custom processing of request and response messages. For example, you might use a handler to add security information to the message or to log message details.
A handler is an instance of a class that ultimately implements the javax.xml.ws.handler.Handler<C extends MessageContext>
interface in terms of the following methods:
void close(MessageContext context)
is called at the conclusion of a Message-Exchange Pattern (MEP) just before the JAX-WS runtime dispatches a message, fault or exception. This method lets a handler clean up any resources used for processing request-only or request-response message exchanges.boolean handleFault(C context)
is invoked for fault message processing. This method returnstrue
when the handler wants to continue handling fault messages; otherwise, it returnsfalse
. It may throwjavax.xml.ws.ProtocolException
(a subclass ofjavax.xml.ws.WebServiceException
) orjava.lang.RuntimeException
to cause the JAX-WS runtime to cease the handler's fault processing and dispatch the fault.boolean handleMessage(C context)
is invoked for normal processing of inbound and outbound messages. This method returnstrue
when the handler wants to continue handling such messages; otherwise, it returnsfalse
. It may throwProtocolException
orRuntimeException
to cause the JAX-WS runtime to cease the handler's normal message processing and generate a fault.
Each method is called with a javax.xml.ws.handler.MessageContext
or subinterface argument that stores a map of properties for handlers to use to communicate with each other and for other purposes. For example, MessageContext.MESSAGE_OUTBOUND_PROPERTY
stores a java.lang.Boolean
object that identifies a message's direction. During a request (from client to Web service), this property's value is Boolean.TRUE
from a client handler's perspective and Boolean.FALSE
from a Web service handler's perspective.
JAX-WS supports logical and protocol handlers. A logical handler is independent of the message protocol (it only has access to the message payload) and is associated with the javax.xml.ws.handler.LogicalMessageContext
and javax.xml.ws.handler.LogicalHandler<C extends LogicalMessageContext>
interfaces. In contrast, a protocol handler is tied to a specific protocol; JAX-WS supports SOAP protocol handlers with the javax.xml.ws.handler.soap.SOAPMessageContext
and javax.xml.ws.handler.soap.SOAPHandler
interfaces.
Logging SOAP messages to standard output
You need to work with SOAPMessageContext
and SOAPHandler
to log the flow of messages. Listing 4 presents a SOAPLoggingHandler
class that implements SOAPHandler<SOAPMessageContext>
to log the flow of SOAP messages by outputting them to the standard output stream.
Listing 4. Logging SOAP messages to standard output
import java.io.IOException;
import java.io.PrintStream;
import java.util.Map;
import java.util.Set;
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;
public final class SOAPLoggingHandler implements SOAPHandler<SOAPMessageContext>
{
private static PrintStream out = System.out;
@Override
public Set<QName> getHeaders()
{
return null;
}
@Override
public void close(MessageContext messageContext)
{
}
@Override
public boolean handleFault(SOAPMessageContext soapmc)
{
log(soapmc);
return true;
}
@Override
public boolean handleMessage(SOAPMessageContext soapmc)
{
log(soapmc);
return true;
}
private void log(SOAPMessageContext soapmc)
{
Boolean outboundProperty = (Boolean)
soapmc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (outboundProperty.booleanValue())
out.println("Outbound message:");
else
out.println("Inbound message:");
SOAPMessage soapm = soapmc.getMessage();
try
{
soapm.writeTo(out);
out.println("\n");
}
catch (IOException|SOAPException e)
{
out.println("Handler exception: " + e);
}
}
}
SOAPLoggingHandler
first declares a java.io.PrintStream
field named out
that identifies the destination. Although System.out
is assigned to out
, you can assign a different output stream to this field for logging SOAP messages to another destination.
SOAPHandler
introduces a Set<QName> getHeaders()
method for informing the JAX-WS runtime about the SOAP headers that the handler is responsible for processing. This method returns a set of qualified names for those SOAP message header blocks that the handler can process. Although we must implement this method, it returns null
because there are no headers to process.
The overriding close()
method does nothing because there are no resources that need to be cleaned up. In contrast, handleFault()
and handleMessage()
invoke the private log()
method to log a SOAP message.
The log()
method uses its SOAPMessageContext
argument to obtain the value of the property identified as MessageContext.MESSAGE_OUTBOUND_PROPERTY
. The return value determines whether an Inbound message
string or an Outbound message
string is logged. log()
next uses this argument to invoke the SOAPMessage getMessage()
method, which returns a SOAPMessage
object whose write(Object o)
method is called to write the SOAP message to the stream identified by out
.