12.4. Going FurtherAlthough you can now do everything in SOAP you knew how to do in XML-RPC, there is a lot more to SOAP. As I said in the beginning of the chapter, two important things that SOAP brings to the table are the ability to use custom parameters with a minimal amount of effort, and more advanced fault handling. In this section, I cover both of these topics. 12.4.1. Custom Parameter TypesThe most limiting thing with the CD catalog, at least at this point, is that it stores only the title and artist for a given CD. It is much more realistic to have an object (or set of objects) that represents a CD with the title, artist, label, track listings, perhaps a genre, and all sorts of other information. I'm not going to build this entire structure, but will move from a title and artist to a CD object with a title, artist, and label. This object needs to be passed from the client to the server and back, and demonstrates how SOAP can handle these custom types. Example 12-8 shows this new class. Example 12-8. The CD classpackage javaxml2; public class CD { /** The title of the CD */ private String title; /** The artist performing on the CD */ private String artist; /** The label of the CD */ private String label; public CD( ) { // Default constructor } public CD(String title, String artist, String label) { this.title = title; this.artist = artist; this.label = label; } public String getTitle( ) { return title; } public void setTitle(String title) { this.title = title; } public String getArtist( ) { return artist; } public void setArtist(String artist) { this.artist = artist; } public String getLabel( ) { return label; } public void setLabel(String label) { this.label = label; } public String toString( ) { return "'" + title + "' by " + artist + ", on " + label; } } This requires a whole slew of changes to the CDCatalog class as well. Example 12-9 shows a modified version of this class with the changes that use the new CD support class highlighted. Example 12-9. An updated CDCatalog classpackage javaxml2; import java.util.Hashtable; public class CDCatalog { /** The CDs, by title */ private Hashtable catalog; public CDCatalog( ) { catalog = new Hashtable( ); // Seed the catalog addCD(new CD("Nickel Creek", "Nickel Creek", "Sugar Hill")); addCD(new CD("Let it Fall", "Sean Watkins", "Sugar Hill")); addCD(new CD("Aerial Boundaries", "Michael Hedges", "Windham Hill")); addCD(new CD("Taproot", "Michael Hedges", "Windham Hill")); } public void addCD(CD cd) { if (cd == null) { throw new IllegalArgumentException("The CD object cannot be null."); } catalog.put(cd.getTitle( ), cd); } public CD getCD(String title) { if (title == null) { throw new IllegalArgumentException("Title cannot be null."); } // Return the requested CD return (CD)catalog.get(title); } public Hashtable list( ) { return catalog; } } In addition to the obvious changes, I've also updated the old getArtist(String title) method to getCD(String title), and made the return value a CD object. This means the SOAP server will need to serialize and deserialize this new class, and the client will be updated. First, I look at an updated deployment descriptor that details the serialization issues related to this custom type. Add the following lines to the deployment descriptor for the CD catalog, as well as changing the available method names to match the updated CDCatalog class: <isd:service xmlns:isd="http://xml.apache.org/xml-soap/deployment" id="urn:cd-catalog" > <isd:provider type="java" scope="Application" methods="addCD getCD list" > <isd:java class="javaxml2.CDCatalog" static="false" /> </isd:provider> <isd:faultListener>org.apache.soap.server.DOMFaultListener</isd:faultListener> <isd:mappings> <isd:map encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:x="urn:cd-catalog-demo" qname="x:cd" javaType="javaxml2.CD" java2XMLClassName="org.apache.soap.encoding.soapenc.BeanSerializer" xml2JavaClassName="org.apache.soap.encoding.soapenc.BeanSerializer"/> </isd:mappings> </isd:service> The new element, mappings, specifies how a SOAP server should handle custom parameters such as the CD class. First, define a map element for each custom parameter type. For the encodingStyle attribute, at least as of Apache SOAP 2.2, you should always supply the value http://schemas.xmlsoap.org/soap/encoding/, the only encoding currently supported. You need to supply a namespace for the custom type and then the name of the class, with this namespace prefix, for the type. In my case, I used a "dummy" namespace and the simple prefix "x" for this purpose. Then, using the javaType attribute, supply the actual Java class name: javaxml2.CD in this case. Finally, the magic occurs in the java2XMLClassName and xml2JavaClassName attributes. These specify a class to convert from Java to XML and from XML to Java, respectively. I've used the incredibly handy BeanSerializer class, also provided with Apache SOAP. If your custom parameter is in a JavaBean format, this serializer and deserializer will save you from having to write your own. You need to have a class with a default constructor (remember that I defined an empty, no-args constructor within the CD class), and expose all the data in that class through setXXX and getXXX style methods. Since the CD class fits the bill here, the BeanSerializer works perfectly. NOTE: It's no accident that the CD class follows the JavaBean conventions. Most data classes fit easily into this format, and I knew I wanted to avoid writing my own custom serializer and deserializer. These are a pain to write (not overly difficult, but easy to mess up), and I recommend you go to great lengths to try and use the Bean conventions in your own custom parameters. In many cases, the Bean conventions only require that a default constructor (with no arguments) is present in your class. Now recreate your service jar file. Then, redeploy your service: (gandalf)/javaxml2/Ch12$ java org.apache.soap.server.ServiceManagerClient http://localhost:8080/soap/servlet/rpcrouter xml/CDCatalogDD.xml WARNING: If you have kept your servlet engine running and the service deployed all this time, you'll need to restart the servlet engine to activate the new classes for the SOAP service, and redeploy the service. At this point, all that's left is modifying the client to use the new class and methods. Example 12-10 is an updated version of the client class CDAdder . The changes from the previous version of the class are highlighted. Example 12-10. The updated CDAdder classpackage javaxml2; import java.net.URL; import java.util.Vector; import org.apache.soap.Constants; import org.apache.soap.Fault; import org.apache.soap.SOAPException; import org.apache.soap.encoding.SOAPMappingRegistry; import org.apache.soap.encoding.soapenc.BeanSerializer; import org.apache.soap.rpc.Call; import org.apache.soap.rpc.Parameter; import org.apache.soap.rpc.Response; import org.apache.soap.util.xml.QName; public class CDAdder { public void add(URL url, String title, String artist, String label) throws SOAPException { System.out.println("Adding CD titled '" + title + "' by '" + artist + "', on the label " + label); CD cd = new CD(title, artist, label); // Map this type so SOAP can use it SOAPMappingRegistry registry = new SOAPMappingRegistry( ); BeanSerializer serializer = new BeanSerializer( ); registry.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("urn:cd-catalog-demo", "cd"), CD.class, serializer, serializer); // Build the Call object Call call = new Call( ); call.setSOAPMappingRegistry(registry); call.setTargetObjectURI("urn:cd-catalog"); call.setMethodName("addCD"); call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC); // Set up parameters Vector params = new Vector( ); params.addElement(new Parameter("cd", CD.class, cd, null)); call.setParams(params); // Invoke the call Response response; response = call.invoke(url, ""); if (!response.generatedFault( )) { System.out.println("Successful CD Addition."); } else { Fault fault = response.getFault( ); System.out.println("Error encountered: " + fault.getFaultString( )); } } public static void main(String[] args) { if (args.length != 4) { System.out.println("Usage: java javaxml2.CDAdder [SOAP server URL] " + "\"[CD Title]\" \"[Artist Name]\" \"[CD Label]\""); return; } try { // URL for SOAP server to connect to URL url = new URL(args[0]); // Get values for new CD String title = args[1]; String artist = args[2]; String label = args[3]; // Add the CD CDAdder adder = new CDAdder( ); adder.add(url, title, artist, label); } catch (Exception e) { e.printStackTrace( ); } } } The only really interesting change is in dealing with the mapping of the CD class: // Map this type so SOAP can use it SOAPMappingRegistry registry = new SOAPMappingRegistry( ); BeanSerializer serializer = new BeanSerializer( ); registry.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("urn:cd-catalog-demo", "cd"), CD.class, serializer, serializer); This is how a custom parameter can be encoded and sent across the wire. I already discussed how the BeanSerializer class could be used to handle parameters in the JavaBean format, such as the CD class. To specify that to the server, I used the deployment descriptor; however, now I need to let the client know to use this serializer and deserializer. This is what the SOAPMappingRegistry class allows. The mapTypes( ) method takes in an encoding string (again, using the constant NS_URI_SOAP_ENC is the best idea here), and information about the parameter type a special serialization should be used for. First, a QName is supplied. This is why the odd namespacing was used back in the deployment descriptor; you need to specify the same URN here, as well as the local name of the element (in this case "CD"), then the Java Class object of the class to be serialized (CD.class), and finally the class instance for serialization and deserialization. In the case of the BeanSerializer, the same instance works for both. Once all this is set up in the registry, let the Call object know about it through the setSOAPMapping-Registry( ) method. You can run this class just as before, adding the CD label, and things should work smoothly: C:\javaxml2\build>java javaxml2.CDAdder http://localhost:8080/soap/servlet/rpcrouter "Tony Rice" "Manzanita" "Sugar Hill" Adding CD titled 'Tony Rice' by 'Manzanita', on the label Sugar Hill Successful CD Addition. I'll leave it up to you to modify the CDLister class in the same fashion, and the downloadable samples have this updated class as well. NOTE: You might think that since the CDLister class doesn't deal directly with a CD object (the return value of the list( ) method was a Hashtable), you don't need to make any changes. However, the returned Hashtable contains instances of CD objects. If SOAP doesn't know how to deserialize these, your client is going to give you an error. Therefore, you must specify a SOAPMappingRegistry instance on the Call object to make things work. 12.4.2. Better Error HandlingNow that you're tossing around custom objects, making RPC calls, and generally showing up everyone else in the office, let me talk about a less exciting topic: error handling. In any network transaction, many things can go wrong. The service isn't running, an error occurs on the server, objects can't be found, classes are missing, and a whole lot of other problems can arise. Until now, I just used the fault.getString( ) method to report errors. But this method isn't always very helpful. To see it in action, comment out the following line in the CDCatalog constructor: public CDCatalog( ) { //catalog = new Hashtable( ); // Seed the catalog addCD(new CD("Nickel Creek", "Nickel Creek", "Sugar Hill")); addCD(new CD("Let it Fall", "Sean Watkins", "Sugar Hill")); addCD(new CD("Aerial Boundaries", "Michael Hedges", "Windham Hill")); addCD(new CD("Taproot", "Michael Hedges", "Windham Hill")); } Recompile, restart your server engine, and redeploy. The result is that a NullPointerException occurs when the class constructor tries to add a CD to an uninitialized Hashtable. Running the client will let you know an error has occurred, but not in a very meaningful way: (gandalf)/javaxml2/build$ java javaxml2.CDLister http://localhost:8080/soap/servlet/rpcrouter Listing current CD catalog. Error encountered: Unable to resolve target object: null This isn't exactly the type of information you need to track down the problem. However, the framework is in place to do a better job of error handling; remember the DOMFaultListener you specified as the value of the faultListener element? This is where it comes into play. The returned Fault object in the case of a problem (as in this one) contains a DOM org.w3c.dom.Element with detailed error information. First, add an import statement for java.util.Iterator to your client source code: import java.net.URL; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.Vector; import org.apache.soap.Constants; import org.apache.soap.Fault; import org.apache.soap.SOAPException; import org.apache.soap.encoding.SOAPMappingRegistry; import org.apache.soap.encoding.soapenc.BeanSerializer; import org.apache.soap.rpc.Call; import org.apache.soap.rpc.Parameter; import org.apache.soap.rpc.Response; import org.apache.soap.util.xml.QName; Next, make the following change to how errors are handled in the list( ) method: if (!response.generatedFault( )) { Parameter returnValue = response.getReturnValue( ); Hashtable catalog = (Hashtable)returnValue.getValue( ); Enumeration e = catalog.keys( ); while (e.hasMoreElements( )) { String title = (String)e.nextElement( ); CD cd = (CD)catalog.get(title); System.out.println(" '" + cd.getTitle( ) + "' by " + cd.getArtist( ) + " on the label " + cd.getLabel( )); } } else { Fault fault = response.getFault( ); System.out.println("Error encountered: " + fault.getFaultString( )); Vector entries = fault.getDetailEntries( ); for (Iterator i = entries.iterator(); i.hasNext( ); ) { org.w3c.dom.Element entry = (org.w3c.dom.Element)i.next( ); System.out.println(entry.getFirstChild().getNodeValue( )); } } By using the getDetailEntries( ) method, you get access to the raw data supplied by the SOAP service and server about the problem. The code iterates through these (there is generally only a single element, but it pays to be careful) and grabs the DOM Element contained within each entry. Essentially, here's the XML you are working through: <SOAP-ENV:Fault> <faultcode>SOAP-ENV:Server.BadTargetObjectURI</faultcode> <faultstring>Unable to resolve target object: null</faultstring> <stacktrace>Here's what we want!</stackTrace> </SOAP-ENV:Fault> In other words, the Fault object gives you access to the portion of the SOAP envelope that deals with errors. Additionally, Apache SOAP provides a Java stack trace if errors occur, and that provides the detailed information needed to troubleshoot problems. By grabbing the stackTrace element and printing the Text node's value from that Element, your client will now print out the stack trace from the server. Compile these changes and rerun the client. You should get the following output: C:\javaxml2\build>java javaxml2.CDLister http://localhost:8080/soap/servlet/rpcr outer Listing current CD catalog. Error encountered: Unable to resolve target object: null java.lang.NullPointerException at javaxml2.CDCatalog.addCD(CDCatalog.java:24) at javaxml2.CDCatalog.<init>(CDCatalog.java:14) at java.lang.Class.newInstance0(Native Method) at java.lang.Class.newInstance(Class.java:237) This goes on for a bit, but you can see the juicy bits of information indicating that a NullPointerException occurred, and even get the line numbers on the server classes where the problems happened. The result of this fairly minor change is a much more robust means of handling errors. That should prepare you for tracking down bugs on your server classes. Oh, and be sure to change your CDCatalog class back to a version that won't cause these errors before moving on! Copyright © 2002 O'Reilly & Associates. All rights reserved. |
|