Consumir un servicio web con Delphi 10.2 Starter y cURL

Hola amigos,

En ésta entrada les hablaré acerca de la versión ‘gratuita’ de Delphi llamada Delphi Starter Edition, versión 10.2 Tokyo la cual nos permite, según su contrato de licencia, generar aplicaciones comerciales aunque con algunas limitaciones propias de éste tipo de ediciones.

Si quieren conocer acerca de éste producto, su licencia de uso y las limitaciones propias de la versión, les proporciono una lista de enlaces que me parecen muy interesantes.

Quiero mencionar que la intención de ésta entrada es la de mostrar que con ésta versión podemos comenzar a desarrollar soluciones reales y no solo el famoso «Hola Mundo». Por supuesto al ser un producto gratuito tiene algunas limitaciones pero nada que nos evite hacer uso de nuestras capacidades y dar forma a aplicaciones de alto rendimiento.

Para ésta entrada les sugiero que descarguen el programa soapUI el cual es open source  y que nos facilitará la vida al generar un cliente para el consumo de Servicios Web así como descargar e instalar Delphi 10.2 Tokio Starter.

Una de las limitantes de la versión Starter es que no cuenta con el asistente para la importación del archivo WSDL el cual genera la unidad que implementa los métodos del Servicio Web para su consumo y aunque ésta unidad se puede obtener vía línea de comandos he querido hacerlo con una solución alternativa a través de un proyecto llamado cURL el cual está orientado a la transferencia de archivos y que soporta varios protocolos como FTP, FTPS, HTTP, HTTPS, TFTP, SCP, SFTP, Telnet, DICT, FILE y LDAP entre otros.

Cabe mencionar que éste proyecto lo conocí hace unos años, para ser exactos el 14 de Marzo de 2011, cuando nuestro amigo Domingo Seaone  publicó en el foro delphiaccess punto com una implementación de ésta biblioteca desarrollando una unidad con Delphi y que me ha permitido dar solución a varios requerimientos de clientes a lo largo de éstos 6 años que han pasado.

Pues bien, comenzaremos por obtener los archivos XML de Request y Response del Servicio Web a través de la aplicación soapUI, para ello usaremos el Servicio Web del Banco de México para consultar el tipo de cambio del peso frente al Dólar, el Euro, la Libra Esterlina y el Yen. La url del servicio web es la siguiente:

http://www.banxico.org.mx/DgieWSWeb/DgieWS?WSDL

Creamos un nuevo proyecto SOAP seleccionamos el método tiposDeCambioBanxico y enviamos la petición, el Servicio Web nos regresará la información en un archivo soap XML conteniendo los diferentes tipos de cambio como se muestra en la imagen siguiente:

La versión Starter de Delphi tampoco incluye el asistente XML Data Binding por lo que tenemos que construir el REQUEST y el RESPONSE de forma manual, bueno, no todo en la vida es fácil, pero veamos la situación como una buena oportunidad para aprender algo nuevo. 🙂

Ya estamos listos para comenzar nuestro tutorial, como primer paso vamos a crear una clase que generará la estructura del mensaje de petición SOAP para enviarla a través de cURL al servidor del Servicio Web y serializar la respuesta SOAP para obtener (en este caso)  los tipos de cambio disponibles del peso mexicano contra otras monedas.

La clase contendrá las propiedades, variables y métodos necesarios para el consumo del Servicio Web como se muestra a continuación.

TRequestResponse = class

type

  TRequestResponse = class
  private
    xmlArch: IXMLDocument;
    envelopeSOAP: IXMLNode;

    Curl: TCURL;
    curlResult: CURLcode;
    curlError: string;

    Stream: TMemoryStream;
    StreamOut: TMemoryStream;

    function creaRequest: IXMLDocument;
    function enviaRequest(url, Post: AnsiString; out Reply: AnsiString): Boolean;
    function RecursiveFindNode(ANode: IXMLNode; const SearchNodeName: string): IXMLNode;
    function buscaTC(xml: IXMLDocument): TStrings;

  public
    function ConsultaTipoDeCambio(endPoint: AnsiString): TStrings;

  end;

El método creaRequest genera el mensaje de petición SOAP utilizando el objeto IXMLDocument el cual se encargará de dar el formato adecuado para poder enviarlo al servidor donde está alojado el Servicio Web que queremos consumir.

function TRequestResponse.creaRequest: IXMLDocument;

function TRequestResponse.creaRequest: IXMLDocument;
begin
  xmlArch := NewXMLDocument;
  xmlArch.Encoding := 'UTF-8';
  envelopeSOAP := xmlArch.AddChild('soapenv:Envelope');
  with envelopeSOAP do
  begin
    Attributes['xmlns:ws'] := 'http://ws.dgie.banxico.org.mx';
    Attributes['xmlns:soapenv'] := 'http://schemas.xmlsoap.org/soap/envelope/';
    Attributes['xmlns:xsd'] := 'http://www.w3.org/2001/XMLSchema';
    Attributes['xmlns:xsi'] := 'http://www.w3.org/2001/XMLSchema-instance';
    AddChild('soapenv:Header');
    with AddChild('soapenv:Body') do
    begin
      with AddChild('ws:tiposDeCambioBanxico') do
      begin
        Attributes['soapenv:encodingStyle'] := 'http://schemas.xmlsoap.org/soap/encoding/';
      end;
    end;
  end;
  Result := xmlArch;
end;

Con éste código se genera el mensaje SOAP necesario para realizar la petición al Servicio Web y que será enviado por medio de cURL.

Por supuesto que habrá formas más efectivas de hacer esto, sin embargo ésta me parece a mi la más simple y controlada.

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
                  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ws="http://ws.dgie.banxico.org.mx">
	<soapenv:Header/>
	<soapenv:Body>
		<ws:tiposDeCambioBanxico soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
	</soapenv:Body>
</soapenv:Envelope>

A continuación agregaremos la unidad creada por mi amigo Domingo Seoane la cual encapsula el método para enviar la petición al Servicio Web, dicha unidad se llama uCurl.pas la cual publicó en DelphiAccess.

A continuación agregamos la función para realizar la llamada a dicha unidad y que llamaremos enviaRequest. En ésta función necesitaremos la URL del Servicio Web y el Request que hemos creado con la función creaRequest, dicha función regresará dos valores, uno de tipo out donde obtendrémos el Response del Servicio Web y un boolean que nos indicará si la función fué procesada correctamente o no.

function TRequestResponse.enviaRequest(url, Post: AnsiString; out Reply: AnsiString): Boolean;

function TRequestResponse.enviaRequest(url, Post: AnsiString; out Reply: AnsiString): Boolean;

//******************************************************************************
      function ReadFromStream(Buffer: PAnsiChar; Size, Count: Integer;
        Stream: TStream): Integer; cdecl;
      begin
        Result:= Stream.Read(Buffer^,Size*Count) div Size;
      end;

      function SaveToStream(Buffer: PAnsiChar; Size, Count: Integer;
        Stream: TStream): Integer; cdecl;
      begin
        Result:= Stream.Write(Buffer^,Size*Count) div Size;
      end;

      function MemoryStreamToString(M:TMemoryStream): AnsiString;
      begin
        SetString(Result, PAnsiChar(M.Memory), M.Size);
      end;

      procedure ErrorCurl(const mensaje: string);
      begin
        raise Exception.Create(mensaje);
        exit;
      end;
//******************************************************************************

begin
  Result:= false;
  Curl:= curl_easy_init;
  if Curl <> nil then
  try
    if curl_easy_setopt(Curl, CURLOPT_VERBOSE, TRUE) <> CURLE_OK then
      ErrorCurl('No se pudo asignar CURLOPT_VERBOSE.');
    if curl_easy_setopt(Curl, CURLOPT_USE_SSL, CURLUSESSL_ALL) <> CURLE_OK then
      ErrorCurl('No se pudo asignar CURLOPT_USE_SSL.');
    if curl_easy_setopt(Curl, CURLOPT_SSL_VERIFYPEER, FALSE) <> CURLE_OK then
      ErrorCurl('No se pudo asignar CURLOPT_SSL_VERIFYPEER.');
    if curl_easy_setopt(Curl, CURLOPT_URL, PAnsiChar(url)) <> CURLE_OK then
      ErrorCurl('No se pudo asignar CURLOPT_URL.');
    if curl_easy_setopt(Curl, CURLOPT_POST, 1) <> CURLE_OK then
      ErrorCurl('No se pudo asignar CURLOPT_POST.');
    if curl_easy_setopt(Curl, CURLOPT_READFUNCTION, @ReadFromStream) <> CURLE_OK then
      ErrorCurl('No se pudo asignar CURLOPT_READFUNCTION.');
    if curl_easy_setopt(Curl, CURLOPT_WRITEFUNCTION, @SaveToStream) <> CURLE_OK then
      ErrorCurl('No se pudo asignar CURLOPT_WRITEFUNCTION.');

    Stream    := TMemoryStream.Create;
    StreamOut := TMemoryStream.Create;

    try
      with TStringList.Create do
      try
        Add(unicodeString(Post));
        SaveToStream(Stream);
      finally
        Free;
      end;
      Stream.Position:= 0;
      if curl_easy_setopt(Curl, CURLOPT_INFILE, Stream) <> CURLE_OK then
        ErrorCurl('No se pudo asignar CURLOPT_INFILE.');
      if curl_easy_setopt(Curl, CURLOPT_POSTFIELDSIZE, Stream.Size) <> CURLE_OK then
        ErrorCurl('No se pudo asignar CURLOPT_POSTFIELDSIZE.');
      Header := nil;
      Header := curl_slist_append(Header, 'SOAPAction: ""');
      Header := curl_slist_append(Header, 'Accept-Encoding: gzip, deflate');
      Header := curl_slist_append(Header, 'Content-Type: text/xml;charset=UTF-8;');
      Header := curl_slist_append(Header, 'User-Agent: Apache-HttpClient/4.1.1 (java 1.5)');
      try
        if curl_easy_setopt(Curl, CURLOPT_FILE, StreamOut) <> CURLE_OK then
          ErrorCurl('No se pudo asignar CURLOPT_FILE.');
        if curl_easy_setopt(Curl, CURLOPT_HTTPHEADER, Header) <> CURLE_OK then
          ErrorCurl('No se pudo asignar CURLOPT_HTTPHEADER.');

        curlResult := curl_easy_perform(Curl);

        if curlResult = CURLE_OK then
        begin
          Reply := MemoryStreamToString(StreamOut);
          curlError := '';
          result := true;
        end
        else begin
               curlError := curl_easy_strerror(curlResult);
               result := false;
        end;

      finally
        curl_slist_free_all(Header);
      end;
    finally
      Stream.Free;
      StreamOut.Free;
    end;
  finally
    curl_easy_cleanup(Curl);
  end;
end;

Ya estamos a punto de terminar la clase que consumirá el Servicio Web, solo nos falta serializar el Response del Servicio Web lo cual harémos manualmente debido a que tenemos ciertas limitantes al no tener disponible el asistente XML Data Binding en el IDE de nuestro Delphi Starter. Cabe mencionar que si ustedes tienen una forma más elegante de hacerlo, no duden en retroalimentarnos para enriquecer éste tutorial.

Para ello vamos a utilizar dos funciones, una que tome prestada del sitio  stackoverflow y una que escribiremos nosotros mismos, dichas funciones son

function RecursiveFindNode(ANode: IXMLNode; const SearchNodeName: string): IXMLNode;

{
   Función basada en la original que se tomó del siguiente enlace
   http://stackoverflow.com/questions/28901757/delphi-find-xml-node
}
function TRequestResponse.RecursiveFindNode(ANode: IXMLNode; const SearchNodeName: string): IXMLNode;
var
  I: Integer;
begin
  if CompareText(ANode.NodeName, SearchNodeName) = 0 then
      Result := ANode
  else if not Assigned(ANode.ChildNodes) then
          Result := nil
       else begin
              for I := 0 to ANode.ChildNodes.Count - 1 do
              begin
                Result := RecursiveFindNode(ANode.ChildNodes[I], SearchNodeName);
                if Assigned(Result) then
                   Exit;
              end;
       end;
end;

function buscaTC(xml: IXMLDocument): TStrings;

function TRequestResponse.buscaTC(xml: IXMLDocument): TStrings;
var
  I: integer;
  nodoData: IXMLNode;
  Linea: string;
begin
  Result := TStringList.Create;
  Result.Text := '';
  nodoData := xml.ChildNodes[1].ChildNodes[1];
  if nodoData <> nil then
  begin
    for I := 0 to nodoData.ChildNodes.Count-1 do
    begin
      if nodoData.ChildNodes[I].NodeName = 'bm:Series' then
      begin
         Result.Add(nodoData.ChildNodes[I].Attributes['TITULO'] + #9 +
                    nodoData.ChildNodes[I].Attributes['BANXICO_UNIT_TYPE'] + #9 +
                    nodoData.ChildNodes[I].ChildNodes[0].Attributes['TIME_PERIOD'] + #9 +
                    nodoData.ChildNodes[I].ChildNodes[0].Attributes['OBS_VALUE'] );
      end;
    end;
  end;
end;

Por último vamos a ewscribir el código de la función principal de nuestra clase llamado ConsultaTipoDeCambio y que requiere de la URL del Servicio Web la cual enviaremos desde la forma principal.

function TRequestResponse.ConsultaTipoDeCambio(endPoint: AnsiString): TStrings;

function TRequestResponse.ConsultaTipoDeCambio(endPoint: AnsiString): TStrings;
var
  nodoBody: IXMLNode;
  nodoResult: IXMLNode;
  xml: IXMLDocument;
  response: AnsiString;

begin
  xml := creaRequest;
  xml.XML.SaveToFile(ExtractFilePath(Application.ExeName) + 'request_' +
                               formatDateTime('yyyymmdd_hhnnss',now) + '.xml' );
  if sendRequest(endPoint, AnsiString(xml.XML.Text), response) then
  begin
    try
      xmlArch := NewXMLDocument;
      xmlArch.XML.Text := response;
      xmlArch.Active := true;
      nodoBody := xmlArch.ChildNodes[1].ChildNodes[1];
      if nodoBody <> nil then
      begin
        nodoResult := RecursiveFindNode(NodoBody, 'result');
        if nodoBody <> nil then
        begin
          xml := TXMLDocument.Create(nil);
          xml.XML.Text := nodoResult.ChildNodes[0].NodeValue;
          xml.Active := true;
          xml.XML.SaveToFile(ExtractFilePath(Application.ExeName) +
                                                'response.xml', TEncoding.ANSI);
          Result := BuscaTC(xml);
        end
        else begin
               Result.Text := 'ERROR: No se pudo acceder al nodo "Result"';
        end;
      end
      else begin
             Result.Text := 'ERROR: No se pudo acceder al nodo "Body"';
      end;
    except
      on e: exception do
         Result.Text := 'ERROR: ' + e.Message;
    end;
  end;

end;

Ya tenemos nuestra unidad uXMLClass.pas la cual contiene la clase TRequestResponse completa y lista para ser usada por el programa principal.

En la forma principal vamos a agregar dos componentes TButton y un componente TMemo. Perdón pero suelo ser minimalista 😀

Acomodamos los componentes a nuestro gusto y escribiremos el siguiente código en el evento OnClick del componente TButton.

procedure TForm1.Button1Click(Sender: TObject);
var
  ReqResp: TRequestResponse;
  retorno: TStrings;
begin
  ReqResp := TRequestResponse.Create;
  retorno := TStringList.Create;
  retorno := ReqResp.ConsultaTipoDeCambio(AnsiString('http://www.banxico.org.mx/DgieWSWeb/DgieWS'));
  Memo1.Lines := retorno;
end;

Finalmente ejecutamos nuestra aplicación y deberíamos obtener la siguiente respuesta.

Con ésto concluimos éste pequeño tutorial donde pretendemos mostrar que se puede trabajar con ésta edición de Delphi y crear aplicaciones listas para dar solución a requerimientos reales. Por supuesto que hay de requerimientos a requerimientos pero la idea es comenzar nuestro «propio negocio» y poder obtener ingresos suficientes para adquirir una de las versiones de pago y dar el siguiente paso para que nuestro negocio se reafirme y podamos dar soluciones completas y mas profesionales.

Antes de despedirme les quiero decir que vamos a estar desarrollando algunos temas con Delphi Starter con lo que pretendemos aportar nuestro granito de arena, si tienen algún tema que pueda ser creado con ésta versión con gusto lo intentaremos desarrollar.

Les agradeceré que dejen sus comentarios, críticas y/o palabras de aliento que me darán el oxígeno necesario para continuar con ésta serie. 🙂

Hasta la próxima


Todo el código escrito aquí es de libre descarga y utilización solo te pido, si te parece correcto, que menciones su origen y claro cualquier mejora que le hagas publicala que se agradecerá enormemente.

Muchas gracias.

20 comentarios en «Consumir un servicio web con Delphi 10.2 Starter y cURL»

  1. Muy buena entrada Eliseo, no conocia la existencia de esa envoltura de Domingo para utilizar cURL. Confieso que esta herramienta la conoci hace muy poco, y a pesar de parecer una «pequeña utilidad» tiene una potencia enorme. Me alegra que se pueda utilizar facilmente desde Delphi

    Enhorabuena!!

    Responder
    • Gracias amigo Agustín. La verdad es que cURL me ha solucionado más de una tarea. No se que sería de mi sin la ayuda de varios compañeros del foro, no menciono nombres porque no quiero ser injusto con ninguno de ustedes.

      Saludos

      Responder
  2. Eliseo , como siempre soprendiendo con tus valiosos aportes.
    He dejado un poco el Desarrollo, pero viendo todas estas grandes herramientas, es una motivación para que nuevamente reinicie mi actividad como Desarrollador.

    Responder
  3. Eliseo buenas tardes.
    Interesante codigo, al respecto, llegue a tu pagina siguiendo un hilo o comentario tuyo sobre Curl en delphi.
    He tratado desde hace tiempo de hacer funcionar la API de Pushbullet con Delphi 7 sin tener exito.
    La idea es enviar un simple mensaje en delphi medianteel uso de curl y la push notification APi de Pushbullet.
    Vi un ejemplo hecho en Lazarus pero no logre adaptarlo a delphi, use ejemplos de libcurl, tambien intente con ejemplos de idHttp en indy 10 pero tuve error con la libreria de OpenSSL, etc. sin exito alguno.
    Te agradeceria si pudieras orientarme o facilitarme un codigo ejemplo.
    Saludos.

    Responder
      • Hola Eliseo, te envié un email al la dirección que se se muestra en la parte superior de esta pagina, es respecto al ejemplo curl, ojala puedas checarlo.
        Saludos.

        Luis

        Responder

Deja un comentario