The Stock ServerWe'll start with the server. The server is connected to the warehouse database, accepts getStock RPC, and returns the latest status on product availability. SoapEnvelopeThe first class we will look at, SoapEnvelope , is not specific to the server. This class is demonstrated in Listing 9.4. Listing 9.4 SoapEnvelope.javapackage com.psol.stockq; import org.xml.sax.*; import org.xml.sax.helpers.*; public class SoapEnvelope extends XMLFilterImpl { protected static final int NONE = 0, ENVELOPE = 1, HEADER = 2, BODY = 3, FAULT = 4, FAULT_CODE = 5, FAULT_STRING = 6; protected int status = NONE; protected static final String SOAP_URI = "http://schemas.xmlsoap.org/soap/envelope/"; protected StringBuffer buffer = null; protected String[] data = null; public void startDocument() throws SAXException { status = NONE; getContentHandler().startDocument(); } public void startElement(String namespaceURI, String localName, String rawName, Attributes atts) throws SAXException { if(BODY == status) if(localName.equals("Fault") && namespaceURI.equals(SOAP_URI)) { status = FAULT; data = new String[2]; } else getContentHandler().startElement(namespaceURI, localName, rawName, atts); else if(localName.equals("Envelope") && NONE == status) if(namespaceURI.equals(SOAP_URI)) status = ENVELOPE; else throw new SoapException("VersionMismatch", "Unknown SOAP version"); else if(localName.equals("Body") && namespaceURI.equals(SOAP_URI) && ENVELOPE == status) status = BODY; else if(localName.equals("Header") && namespaceURI.equals(SOAP_URI) && ENVELOPE == status) status = HEADER; else if(status == HEADER) { // IMHO it really should be in the SOAP namespace String mu = atts.getValue("mustUnderstand"); if(mu != null && mu.equals("1")) throw new SoapException("MustUnderstand", rawName + " unknown"); } else if(localName.equals("faultcode") && status == FAULT) { status = FAULT_CODE; buffer = new StringBuffer(); } else if(localName.equals("faultstring") && status == FAULT) { status = FAULT_STRING; buffer = new StringBuffer(); } } public void endElement(String namespaceURI, String localName, String rawName) throws SAXException { if(BODY == status) getContentHandler().endElement(namespaceURI, localName, rawName); else if(localName.equals("Envelope") && namespaceURI.equals(SOAP_URI) && ENVELOPE == status) status = NONE; else if(localName.equals("Body") && namespaceURI.equals(SOAP_URI) && BODY == status) status = ENVELOPE; else if(localName.equals("Header") && namespaceURI.equals(SOAP_URI) && HEADER == status) status = ENVELOPE; else if(localName.equals("Fault") && namespaceURI.equals(SOAP_URI) && status == FAULT) throw new SoapException(data[0],data[1]); else if(localName.equals("faultcode") && status == FAULT_CODE) { status = FAULT; data[0] = buffer.toString(); buffer = null; } else if(localName.equals("faultstring") && status == FAULT_STRING) { status = FAULT; data[1] = buffer.toString(); buffer = null; } } public void characters(char[] ch,int start,int len) throws SAXException { if(BODY == status) getContentHandler().characters(ch,start,len); else if(FAULT_CODE == status FAULT_STRING == status) buffer.append(ch,start,len); } public void skippedEntity(String name) throws SAXException { if(BODY == status) getContentHandler().skippedEntity(name); } public void ignorableWhitespace(char[] ch, int start, int len) throws SAXException { if(BODY == status) getContentHandler().ignorableWhitespace(ch,start,len); } public void processingInstruction(String target,String data) throws SAXException { if(BODY == status) getContentHandler().processingInstruction(target,data); } } SoapEnvelope passes most events unmodified to its ContentHandler : public void startDocument() throws SAXException { status = NONE; getContentHandler().startDocument(); } The main methods are startElement() and endElement() . The filter intercepts events related to SOAP elements but passes other events unmodified. SOAP-ENV:Header requires special attention. You will remember that header elements are not defined by SOAP. However, the header might influence how the server should process the request ”for example, when a client makes a request within the context of a transaction, it might impact the server response. What happens if a server does not recognize the transaction elements? SOAP suggests you label mandatory elements in the header with a mustUnderstand attribute. The server must either recognize the element or signal an error. The filter enforces this rule: else if(localName.equals("Header") && namespaceURI.equals(SOAP_URI) && ENVELOPE == status) status = HEADER; else if(status == HEADER) { // IMHO it really should be in the SOAP namespace String mu = atts.getValue("mustUnderstand"); if(mu != null && mu.equals("1")) throw new SoapException("MustUnderstand", rawName + " unknown"); } The filter also enforces version control. SOAP uses namespaces for versioning. Elements not in the SOAP namespace indicate a new, incompatible version: else if(localName.equals("Envelope") && NONE == status) if(namespaceURI.equals(SOAP_URI)) status = ENVELOPE; else throw new SoapException("VersionMismatch", "Unknown SOAP version"); SoapServiceSoapService , in Listing 9.5, inherits from a servlet to implement the SOAP protocol. Its descendants must worry about only the RPC. Listing 9.5 SoapService.javapackage com.psol.stockq; import java.io.*; import java.sql.*; import org.xml.sax.*; import javax.servlet.*; import javax.servlet.http.*; import org.xml.sax.helpers.*; public abstract class SoapService extends HttpServlet { public abstract void doSoap(XMLReader reader, InputSource source, XMLWriter writer) throws IOException, SoapException, SAXException; // to optimize, we could manage a pool of XMLReader public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // check for SOAPAction, ignore its value // because the spec is unclear on what the server // should do with SOAPAction String soapAction = request.getHeader("SOAPAction"); if(null == soapAction) throw new SoapException("Client", "Missing SOAPAction"); XMLReader xmlReader = XMLReaderFactory.createXMLReader( Constants.SAXPARSER); xmlReader.setFeature(Constants.SAXNAMESPACES,true); SoapEnvelope soapEnvelope = new SoapEnvelope(); soapEnvelope.setParent(xmlReader); CharArrayWriter payload = new CharArrayWriter(); payload.write("<?xml version='1.0'?>"); payload.write("<SOAP-ENV:Envelope xmlns:SOAP-ENV='"); payload.write(Constants.SOAPENV_URI); payload.write("'><SOAP-ENV:Body>"); InputSource source = new InputSource(request.getReader()); doSoap(soapEnvelope,source,new XMLWriter(payload)); payload.write("</SOAP-ENV:Body></SOAP-ENV:Envelope>"); Writer writer = response.getWriter(); response.setContentType("text/xml"); payload.writeTo(writer); writer.flush(); } catch(SoapException e) { response.setStatus( HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setContentType("text/xml"); e.writeTo(new XMLWriter(response.getWriter())); response.getWriter().flush(); } catch(SAXException e) { // when SAXException embeds another exception // it does a poor job at returning the embedded // exception message, so extract it response.setStatus( HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setContentType("text/xml"); Exception ex = e.getException() != null ? e.getException() : e; new SoapException("Client",ex.getMessage()). writeTo(new XMLWriter(response.getWriter())); response.getWriter().flush(); } catch(Exception e) { response.setStatus( HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setContentType("text/xml"); new SoapException("Server",e.getMessage()). writeTo(new XMLWriter(response.getWriter())); response.getWriter().flush(); } } } SoapService parses the envelope (through SoapEnvelope ) but delegates processing of the request to its descendants (through a call to doSoap() ). Likewise, it writes the SOAP envelope but lets its descendants write the response. Notice how it creates a parser, turns on namespace processing, and activates the SoapEnvelope as an XML filter: XMLReader xmlReader = XMLReaderFactory.createXMLReader(Constants.SAXPARSER); xmlReader.setFeature(Constants.SAXNAMESPACES,true); SoapEnvelope soapEnvelope = new SoapEnvelope(); soapEnvelope.setParent(xmlReader); Next , it uses SoapEnvelope as if it were the parser itself: doSoap(soapEnvelope,source,new XMLWriter(payload)); XMLWriterXMLWriter , in Listing 9.6, should look familiar. It provides a helper method to escape reserved characters ( < , & , and more). Listing 9.6 XMLWriter.javapackage com.psol.stockq; import java.io.*; public class XMLWriter extends PrintWriter { public XMLWriter(Writer writer) { super(writer); } public void escape(String s) throws IOException { for(int i = 0;i < s.length();i++) { char c = s.charAt(i); if(c == '<') write("<"); else if(c == '&') write("&"); else if(c == '\ '') write("'"); else if(c == '"') write("""); else if(c > '\ u007f') { write("&#"); write(Integer.toString); write(';'); } else write; } } } SoapExceptionSoapException , in Listing 9.7, stores the faultcode and faultstring . It also provides a convenient writeTo() method to write the fault in XML. Listing 9.7 SoapException.javapackage com.psol.stockq; import java.io.*; import org.xml.sax.*; public class SoapException extends SAXException { protected String code; public SoapException(String code,String string) { super(string != null ? string : "Unknown error"); this.code = code; } public String getCode() { return code; } public void writeTo(XMLWriter writer) throws IOException { writer.write("<?xml version='1.0'?>"); writer.write("<SOAP-ENV:Envelope xmlns:SOAP-ENV='"); writer.write(Constants.SOAPENV_URI); writer.write("'><SOAP-ENV:Body>"); writer.write("<SOAP-ENV:Fault><faultcode>SOAP-ENV:"); writer.escape(code); writer.write("</faultcode><faultstring>"); writer.escape(getMessage()); writer.write("</faultstring></SOAP-ENV:Fault>"); writer.write("</SOAP-ENV:Body></SOAP-ENV:Envelope>"); } } DatabaseSo far, we have looked at generic SOAP classes. To study the specifics of the stock server, we'll start with the database. Again, because our focus is on XML, not stock management, I've kept the database simple. It contains a single table, products , which lists products and their availability (negative numbers indicate back orders). Products are identified by their manufacturer name and a product number (sku). Warning This chapter does not include a tool to update inventory levels. You will need to edit them through your database user interface. However, it is probably not a good idea to let retailers remotely manipulate product availability! You want the database to reflect actual levels in the warehouse. StockResponseStockResponse , in Listing 9.8, implements ContentHandler . Because it comes after a SoapEnvelope filter, it never sees the SOAP elements. As far as StockResponse is concerned , the root of the document is getProduct . Listing 9.8 StockResponse.javapackage com.psol.stockq; import java.io.*; import java.sql.*; import org.xml.sax.*; import org.xml.sax.helpers.*; public class StockResponse extends DefaultHandler { protected StringBuffer manufacturer = null, sku = null; protected final static int NONE = 0, GET_STOCK = 1, MANUFACTURER = 2, SKU = 3; protected int status = NONE; public void startDocument() throws SAXException { status = NONE; manufacturer = null; sku = null; } public void startElement(String namespaceURI, String localName, String rawName, Attributes atts) throws SAXException { if(localName.equals("getStock") && namespaceURI.equals(Constants.PSOL_URI) && NONE == status) status = GET_STOCK; else if(rawName.equals("manufacturer") && GET_STOCK == status && null == manufacturer) { manufacturer = new StringBuffer(); status = MANUFACTURER; } else if(rawName.equals("sku") && GET_STOCK == status && null == sku) { sku = new StringBuffer(); status = SKU; } } public void endElement(String namespaceURI, String localName, String rawName) throws SAXException { if(localName.equals("getStock") && namespaceURI.equals(Constants.PSOL_URI) && GET_STOCK == status) status = NONE; else if(rawName.equals("manufacturer") && MANUFACTURER == status) status = GET_STOCK; else if(rawName.equals("sku") && SKU == status) status = GET_STOCK; } public void characters(char[] ch,int start,int len) throws SAXException { if(SKU == status) sku.append(ch,start,len); else if(MANUFACTURER == status) manufacturer.append(ch,start,len); } public void writeResponse(Connection connection, XMLWriter writer) throws SQLException, IOException, SoapException { if(manufacturer == null sku == null) throw new SoapException("Client", "Missing manufacturer or sku"); PreparedStatement stmt = connection.prepareStatement("select level " + "from products where manufacturer=? and sku=?"); try { stmt.setString(1,manufacturer.toString()); stmt.setString(2,sku.toString()); ResultSet rs = stmt.executeQuery(); try { writer.write("<psol:getStockResponse xmlns:psol='"); writer.write(Constants.PSOL_URI); writer.write("'SOAP-ENV:encodingStyle='"); writer.write(Constants.SOAPENCODING_URI); writer.write("'><stockq><manufacturer>"); writer.write(manufacturer.toString()); writer.write("</manufacturer><sku>"); writer.write(sku.toString()); writer.write("</sku><available>"); if(rs.next()) { writer.write("true</available><level>"); writer.escape(rs.getString(1)); } else writer.write("false</available><level>0"); writer.write("</level></stockq>"); writer.write("</psol:getStockResponse>"); } finally { rs.close(); } } finally { stmt.close(); } } } StockResponse is also responsible for querying the database and writing the response in the writeResponse() method. Notice that in so doing, it ignores the SOAP envelope that will be added by SoapService : public void writeResponse(Connection connection, XMLWriter writer) throws SQLException, IOException, SoapException { if(manufacturer == null sku == null) throw new SoapException("Client", "Missing manufacturer or sku"); PreparedStatement stmt = connection.prepareStatement("select level " + "from products where manufacturer=? and sku=?"); try { stmt.setString(1,manufacturer.toString()); stmt.setString(2,sku.toString()); ResultSet rs = stmt.executeQuery(); try { writer.write("<psol:getStockResponse xmlns:psol='"); writer.write(Constants.PSOL_URI); writer.write("'SOAP-ENV:encodingStyle='"); writer.write(Constants.SOAPENCODING_URI); writer.write("'><stockq><manufacturer>"); writer.write(manufacturer.toString()); writer.write("</manufacturer><sku>"); writer.write(sku.toString()); writer.write("</sku><available>"); if(rs.next()) { writer.write("true</available><level>"); writer.escape(rs.getString(1)); } else writer.write("false</available><level>0"); writer.write("</level></stockq>"); writer.write("</psol:getStockResponse>"); } finally { rs.close(); } } finally { stmt.close(); } } StockQServiceStockQService , in Listing 9.9, is the actual servlet. It parses SOAP requests and writes the response through StockResponse . Listing 9.9 StockQService.javapackage com.psol.stockq; import java.io.*; import java.sql.*; import org.xml.sax.*; import javax.servlet.*; public class StockQService extends SoapService { public void init() throws ServletException { try { Class.forName(getInitParameter("driver")); } catch(ClassNotFoundException e) { throw new ServletException(e); } } public void doSoap(XMLReader reader, InputSource source, XMLWriter writer) throws IOException, SoapException, SAXException { StockResponse response = new StockResponse(); reader.setContentHandler(response); reader.parse(source); try { String url = getInitParameter("url"), username = getInitParameter("username"), password = getInitParameter("password"); Connection connection = DriverManager.getConnection(url,username,password); try { response.writeResponse(connection,writer); } finally { connection.close(); } } catch(SQLException e) { throw new SoapException("Server", "SQL: " + e.getMessage()); } } } Warning For SOAP, a one-to-one mapping between servlets and RPCs is not available. A servlet can accept different RPCs: It should recognize them by their names . |