Simple Object Access Protocol (SOAP) has always been a technology that infuriated me. Somebody had an idea for a simple mechanism where a remote procedure call was asked and answered in XML-formatted messages. Yet, as with all things XML, a committee was formed, many trips to conferences were funded, and complicated unreadable specifications were drafted. Things like WSDL, UDDI, and automating the whole thing from a web services framework appeared. All for an originally simple concept.

So, why don’t we just go back to the basics? All we need to do is compose the XML request, and parse the XML response. Hey, Scala is good at these things. Although I am not a fan of scala.xml, it is actually good at filling in the values of a static XML template. And its XML projection, similar to XPath, is also good at grabbing values from the response.

The snippet below is the client. Yes, it is the ENTIRE client. Look at the test at the bottom to see how to use Scala’s literal XML support and projections. Quite Simple, eh?

/* Minimalist SOAP client
 *
 * Authors:
 *   Bob Jamison
 *
 * Copyright (C) 2010 Bob Jamison
 *
 *  This file is part of the Pedro library.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 3 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

import scala.xml.{Elem,XML}

class SoapClient {
  private def error(msg: String) = {
    println("SoapClient error: " + msg)
  }

  def wrap(xml: Elem) : String = {
    val buf = new StringBuilder
    buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n")
    buf.append("<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n")
    buf.append("<SOAP-ENV:Body>\n")
    buf.append(xml.toString)
    buf.append("\n</SOAP-ENV:Body>\n")
    buf.append("</SOAP-ENV:Envelope>\n")
    buf.toString
  }

  def sendMessage(host: String, req: Elem) : Option[Elem] = {
    val url = new java.net.URL(host)
    val outs = wrap(req).getBytes
    val conn = url.openConnection.asInstanceOf[java.net.HttpURLConnection]
    try {
      conn.setRequestMethod("POST")
      conn.setDoOutput(true)
      conn.setRequestProperty("Content-Length", outs.length.toString)
      conn.setRequestProperty("Content-Type", "text/xml")
      conn.getOutputStream.write(outs)
      conn.getOutputStream.close
      Some(XML.load(conn.getInputStream))
    }
    catch {
      case e: Exception => error("post: " + e)
        error("post:" + scala.io.Source.fromInputStream(conn.getErrorStream).mkString)
        None
    }
  }
}

object SoapTest {
  def doTest1 {
    val host = "https://apitest.authorize.net/soap/v1/Service.asmx"
    val req  = <IsAlive xmlns="https://api.authorize.net/soap/v1/"/>
    val cli = new SoapClient
    println("##### request:\n" + cli.wrap(req))
    val resp = cli.sendMessage(host, req)
    if (resp.isDefined) {
      println("##### response:\n" + resp.get.toString)
    }
  }

  def doTest2 {
    val host = "http://ws.cdyne.com/WeatherWS/Weather.asmx"
    val req  = <GetCityForecastByZIP xmlns="http://ws.cdyne.com/WeatherWS/">
               <ZIP>77058</ZIP>
               </GetCityForecastByZIP>
    val cli = new SoapClient
    println("##### request:\n" + cli.wrap(req))
    val resp = cli.sendMessage(host, req)
    if (resp.isDefined) {
      println("##### response:\n")
      (resp.get  \\ "Forecast").foreach(elem => {
        println("#########\n" + elem.toString)
      })
    }
  }

  def main(args: Array[String]) {
    doTest1
    doTest2
  }
}