Web services in Java SE, Part 4: SOAP with Attachments API for Java

Learn about SAAJ and other advanced Java SE Web service features

1 2 3 4 5 6 Page 3
Page 3 of 6

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

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 returns true when the handler wants to continue handling fault messages; otherwise, it returns false. It may throw javax.xml.ws.ProtocolException (a subclass of javax.xml.ws.WebServiceException) or java.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 returns true when the handler wants to continue handling such messages; otherwise, it returns false. It may throw ProtocolException or RuntimeException 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.

1 2 3 4 5 6 Page 3
Page 3 of 6