# -*- mode: qore; indent-tabs-mode: nil -*-
#! @file SoapClient.qc SOAP Client implementation based on the WSDL classes

# a minimal SOAP client using WSDL, XSD, SOAP support implemented in WSDL.qc
# by David Nichols

# to use this class: %include WSDL.qc
#                    %include SoapClient.qc
#                    %include MultiPartMessage.qc
#
# the constructor takes named arguments in the form of a hash
# vaild arguments are:
# required keys: one of: "wsdl" or "wsdl_file"
#                                  : a string defining the WSDL or the URL of
#                                    the WSDL
# optional keys:         "service" : the name of the "portType" to use (if
#                                    more than 1 portType is defined in the
#                                    WSDL then this key is mandatory
#                        "url"     : to override the URL defined in the WSDL
#                        "headers" : to override any HTTP headers sent in
#                                    outgoing messages
#                        "event_queue" : to set an event queue on the
#                                        HTTPClient
#
# also the following keys can be set to set HTTP options:
#   "connect_timeout", "http_version", "max_redirects", "proxy", "timeout"
#
# create messages by setting up a Qore data structure corresponding to the SOAP
# message.  Exceptions will be thrown if either the outgoing or the response
# message do not corespond to the WSDL.   The exceptions should be fairly verbose
# to allow you to quickly correct any mistakes.
#
# currently the WSDL implementation is fairly basic so any messages using
# unimplemented features of SOAP or XSD will fail.
#
# example: (make sure the files are in the same directory or in the
#           QORE_INCLUDE_DIR path)
#
# %include WSDL.qc
# %include SoapClient.qc
# my SoapClient $sc = new SoapClient(("wsdl" : "http://soap.server.org:8080/my-service?wsdl"));
# my any $result = $sc.call("SubmitDocument", $msg);

# we need qore 0.7.3 or later for parseXMLAsData
# qore 0.8 for hard typing
%requires qore >= 0.8

#! SOAP client class implementation, publically inherits qore's HTTPClient class
class SoapClient inherits HTTPClient {
    #! version of the implementation of this class
    const Version = "0.2.2";
    #! default HTTP headers
    const Headers = ("Accept":"application/soap+xml,text/xml", "User-Agent":("Qore Soap Client " + SoapClient::Version));
    #! option keys passed to the HTTPClient constructor
    const HTTPOptions = ( "connect_timeout", "http_version", "max_redirects", "proxy", "timeout" );

    private {
        WebService $.wsdl;  # web service definition
        string $.svc;   # service name
    }

    public {
        #! target URL
        string $.url;
        #! HTTP headers to use
        hash $.headers = Headers;
    }

    #! creates the object based on a %WSDL which is parsed to a WSDL::WebService object which provides the basis for all communication with this object
    /** one of either the \c wsdl or \c wsdl_file keys is required in the hash given to the constructor or an exception will be thrown
        @param $h valid option keys:\n- \c wsdl: the URL of the web service or a WSDL::WebService object itself\n- \c wsdl_file: a path to use to load the %WSDL and create the WSDL::WebService object\n- \c url: override the target URL given in the %WSDL\n- also all options from SoapClient::HTTPOptions, which are passed to the HTTPClient constructor
      */
    constructor(hash $h) : HTTPClient($h{HTTPOptions}) {
	if (exists $h.wsdl_file && exists $h.wsdl)
	    throw "SOAP-CLIENT-ERROR", "only one of 'wsdl' or 'wsdl_file' keys can be given; both were passed";

	if (exists $h.event_queue)
	    $.setEventQueue($h.event_queue);

	my any $wsdl;
	# get web service definition
	if (exists $h.wsdl_file)
	    $wsdl = WSDLLib::getWSDL($h.wsdl_file, $self, $h.headers);
	else if (exists $h.wsdl)
	    $wsdl = WSDLLib::getWSDL($h.wsdl, $self, $h.headers);
	else
	    throw "SOAP-CLIENT-ERROR", "neither one of required 'wsdl' or 'wsdl_file' keys is present in the hash argument to SoapClient::constructor()";

	if (!exists $wsdl)
	    throw "SOAP-CLIENT-ERROR", "missing wsdl in SoapClient::constructor()";

	$.wsdl = $wsdl instanceof WebService ? $wsdl : new WebService($wsdl, ("http_client" : $self, "http_headers" : $h.headers) + $h.wsdl_opt);

	# set service
	# get list of services in this wsdl
	my any $svcs = keys $.wsdl.portType;
	if (elements $svcs > 1 && !exists $h.service)
	    throw "SOAP-CLIENT-ERROR", sprintf("no 'service' key passed in the option hash argument to SoapClient::constructor() (WSDL defines the following services: %n)", $svcs);

	if (exists $h.service) {
	    if (!inlist($h.service, $svcs))
		throw "SOAP-CLIENT-ERROR", sprintf("service %n is not defined by this WSDL (valid services: %n)", $h.service, $svcs);
	    $.svc = $h.service;
	}
	else
	    $.svc = $svcs[0];

	if (exists $h.url)
	    $.url = $h.url;
	else {
	    if (elements $.wsdl.services.port > 1)
		throw "SOAP-CLIENT-ERROR", sprintf("don't know how to handle more than one port in a WSDL (this WSDL has %n)", keys $.wsdl.services.port);
	    my string $port = (keys $.wsdl.services.port)[0];

	    $.url = $.wsdl.services.port.$port.address;
	}

	$.headers += $h.headers;
	# setup default headers
	if ($.wsdl.isSoap12())
	    $.headers += ("Content-Type":"application/soap+xml");
	else
	    $.headers += ("Content-Type":"text/xml");

	# set URL
	$.setURL($.url);
	#printf("DEBUG: set url to %n\n", $.url);
    }

    #! returns a hash representing the serialized SOAP request for a given WSOperation
    /** the returned hash can be passed to makeXMLString() to make the actual SOAP message
        @param $operation the SOAP operation to use to serialize the request; if the operation is not known to the underlying WebService class, an exception will be thrown
        @param $h the operation parameter(s)
        @param $op a reference to return the WSOperation object found
      */
    hash getMsg(string $operation, any $h, reference $op) {
	$op = $.wsdl.portType.$.svc.operations.$operation;
	if (!exists $op)
	    throw "SOAP-CLIENT-ERROR", sprintf("operation %n does not exist (operations defined by service %n: %n)",
					       $operation, $.svc, keys $.wsdl.portType.$.svc.operations);

	return $op.serializeRequest($h, $.headers);
    }

    #! makes a server call with the given operation and arguments and returns the deserialized result
    /** @param $operation the operation name for the SOAP call
        @param $h the operation parameter(s)
        @param $info an optional reference to return technical information about the SOAP call (raw message info and headers)
        @return the deserialized result of the SOAP call to the SOAP server
      */
    any call(string $operation, any $h, any $info) {
	my WSOperation $op;
	my hash $msg = $.getMsg($operation, $h, \$op);

	# we have to write the request key after the HTTPClient::post() call
        on_exit $info.request = $msg;

	$info.response = $.send($msg.body, "POST", $.url, $.headers + $msg.hdr, True, \$info);

	my hash $xmldata = WSDLLib::parseSOAPMessage($info.response, $info.response.body);

	#printf("DEBUG ans=%n\n", $info.response);
	return $op.deserializeResponse($xmldata);
    }

    #! uses SoapClient::call() to transparently serialize the argument and make a call to the given operation and return the deserialized results
    /** @param $op the operation name, which is the method name passed to methodGate()
        @return the deserialized result of the SOAP call to the SOAP server
      */
    any methodGate(string $op) {
        return $.call($op, $argv[0]);
    }
}