Programa de testes com Interface para API NETiZAP – Update 2

Fontes atualizados disponíveis no GITHUB

Através do link https://github.com/siga0984/NETiZAP ,você pode baixar os fontes atualizados relacionados a integração com a API NETIZAP!

NETiZAP.prw -- Classe AdvPL NETIZAP que encapsula as requisições dos serviços 
IzapClient.prw -- Fonte de exemplo e teste de uso da classe com interface gráfica -- SmartClient.
USAZAP.PRW -- Fonte de exemplo de uso da classe para o ERP -- contribuição do desenvolvedor Patrick Zerbinatti

Documentação da Classe

A classe de encapsulamento de requisições e uso do serviço está sendo documentada no GITHUB, no link “Wiki” do Projeto — https://github.com/siga0984/NETiZAP/wiki 😀

Contato com o provedor do serviço

Para maiores informações sobre a forma de provisionamento do serviço, contratação comercial, obtenção de uma chave de testes personalizada e afins, entre em contato com o Thiago, desenvolvedor/provedor dessa API, no telefone/WhatsApp +55(27)99802-2075, ou pelo e-mail thiagopedro.br@gmail.com

E NOVAMENTE DESEJO A TODOS TERABYTES DE SUCESSO !!! 

Agradecimento especial ao nobre colega Patrick Zerbinatti, pelo primeiro pull request no GITHUB para compartilhar um novo fonte de exemplo de uso da classe 😀 

Integração com WhatsAPP através de API – NETiZAP

Introdução

O WhatsApp tornou-se o aplicativo de mensagens instantâneas para Smartphones mais usado no mundo, atingindo a marca de 1 bilhão de usuários em fevereiro de 2016. Em janeiro de 2018, surgiu a versão “Business” do aplicativo, e em agosto do mesmo ano foi disponibilizada uma integração via API WEB, viabilizando integrações automáticas por sistemas, como atendimentos automatizados, robôs de chat e afins. Durante as buscas de onde/como enviar mensagens para WhatsApp usando esta tecnologia, encontrei uma API de integração via HTTP / WebRequests, chamada NETiZAP. Nesse post vamos ver o que é e como usar essa API, usando uma classe de encapsulamento em AdvPL 😀

NETiZAP API

Trata-se de uma API comercial de integração com o WhatsApp que recebe requisições de integração via HTTP (GET/POST), o que a torna muito simples de ser consumida. Basicamente a API disponibiliza até o momento 9 operações :

  • message_send: Envio de mensagem
  • question_send: Envio de mensagem com pergunta
  • file_send: Envio de mensagem com arquivo anexo
  • message_search: Consulta de status de mensagem enviada
  • question_search: Consulta de status de resposta de uma pergunta
  • group_search: Consulta grupos de envio de mensagens registrados para a linha
  • broadcast_search: Consulta listas de broadcast registrados para a linha
  • check_credits: Consulta o status / créditos para o plano de mensagens contratado
  • requests_start: Consulta o consumo e requisições feitas pela API

A API trabalha com strings em UTF-8, cada requisição possui parâmetros enviados pela URL e/ou por POST de formulário, a string retornada para cada requisição está em formato JSON.

O provedor do serviço fornece uma chave de acesso para testes, atrelada a uma linha de origem. Com ela você pode testar o serviço e enviar por exemplo uma mensagem, arquivo ou pergunta para o seu celular. Para uso em produção e implementação de rotinas, você deve entrar em contato com o provedor da NETiZAP e adquirir um plano pago — com uma linha personalizada atrelada a uma chave de acesso exclusiva — ou solicitar uma chave personalizada para testes. Independente do plano adquirido, é proibido usar a API para fazer SPAM, e as regras de uso são supervisionadas e apicadas pela WhatsApp.

Para utilizar comercialmente a API NETiZAP, entre em contato com Thiago — WhatsApp (27) 998022075.

Classe AdvPL NETiZAP

Disponível no GITHUB (https://github.com/siga0984/NETiZAP) — sob licenciamento de uso livre e irrestrito — a classe AdvPL NETIZAP encapsula os tratamentos básicos de requisição para cada um dos serviços disponíveis. O fonte não depende do projeto ZLIB, pode ser baixado e compilado diretamente em qualquer ambiente do Protheus Server com build igual ou posterior a 7.00.131227 — independente da versão do ERP em uso — Protheus 11, Protheus 12 NG, Protheus 12 LG).

A classe disponibiliza cada operação em um método, que encapsula o envio e retorno da requisição para o provedor da API. Os métodos em si não recebem parâmetros diretamente, você deve atribuir os valores nas propriedades necessárias da instância em uso da classe antes de chamar o método.

Ao criar a instância da classe, você deve fornecer a linha registrada para origem das mensagens, e a respectiva chave de acesso, e opcionalmente a porta HTTP do provedor de serviços. O provedor do serviço está publicado no host “api.meuaplicativo.vip”, e para fins de demonstração, os dados abaixo podem ser utilizados:

Linha .............. 5527981049976
Chave de Acesso .... Voz5dWkr5EEhBAbYuwJ2
Porta HTTP ......... 13005

Antes de mais nada, vamos ao exemplo de envio de uma mensagem usando a classe:

USER Function MsgSend()
Local oNetIZap 
Local cResponse

// Cria o objeto client usando a chave de demostração
oNetIZap := NETIZAP():New("5527981049976",'Voz5dWkr5EEhBAbYuwJ2',13005)

// Define as propriedades minimas para envio de mensagem 
// *** Número do Celular (WhatsApp) no formato internacional, apenas nímeros
// por exemplo para um telefone celular de São Paulo, 
// utilize "5511" seguido pelo número da linha a enviar a mensagem
oNetIZap:SetDestiny("5511xxxxxxxxx")

// Informa o texto da mensagem 
// 
oNetIZap:SetText(EncodeUTF8("*Olá!*\nEsta é uma mensagem de _teste_ enviada pelo Protheus."))

// Realiza o envio da mensagem 

If oNetIZap:MessageSend()

  // Pega o JSON de retorno da requisiçao 
  // Cada mensagem enviada retorna um identificador de protocolo 
  // no formato "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX".   
  cResponse := oNetIZap:GetResponse()

  // Mostra na inteface 
  MsgInfo(cResponse,"MessageSend() - Retorno da Requisição")

Else

  // Em caso de falha no envio, recupera o erro 
  MSgStop(oNetIZap:GetLastError(),'MessageSend() - Falha no envio da requisição')

Endif

Return

Em caso de sucesso no envio da mensagem, devemos obter o seguinte resultado:

NETiZap - MEssageSend

O número de protocolo retornado é usado para confirmarmos se a mensagem foi recebida e lida pelo destinatário, usando o método MessageSearch(), vide exemplo abaixo:

USER Function MsgSearch()

Local oNetIZap 
Local cResponse

// Cria o objeto client usando uma chave de demmostração
oNetIZap := NETIZAP():New("5527981049976",'Voz5dWkr5EEhBAbYuwJ2')

// Informa o protococolo da mensagem a ser pesquisada
oNetIZap:SetProtocol("E8891831-2FF4-43B2-8388-2C6D01C7D0A6")

If oNetIZap:MessageSearch()

  cResponse := oNetIZap:GetResponse()
  MsgInfo(cResponse,"MessageSearch() - Retorno da Requisição")

Else

  MsgStop(oNetIZap:GetLastError(),'MessageSearch() - Falha no envio da requisição')

Endif

Return

Em caso de sucesso, devemos receber o seguinte retorno:

NETiZap - MEssageSearch

Agora, vamos ver com mais detalhes cada um dos métodos disponíveis e como cada um deles funciona.

Método MessageSend()

Exige que seja informada a linha de destino da mensagem — usando o método SetDestiny() — usando uma string contendo apenas números: Codigo do País, seguido do DDD e do número de destino, e uma mensagem de texto — usando a codificação/encoding UTF-8 — contendo o texto a ser enviado. É uma boa prática não enviar mensagens maiores que 512 Bytes — para conteúdos maiores envie um arquivo anexo 😀

Para fins de rastreabilidade e controle, opcionalmente podemos informar um valor chamado de “referência” — usando o método SetReference() —  de até 60 caracteres, que será guardado pelo provedor do serviço junto da mensagem. Quando você consultar a mensagem usando o protocolo fornecido no envio, este número também é retornado — vide exemplo acima,  como não foi informado um número de referência na mensagem enviada, o valor de “reference” retornado na consulta da mensagem veio em branco.

É possível o envio para grupos de mensagens e listas de broadcast registradas no provedor para a linha contratada. Para isso, usamos os métodos GroupSearch() e BroadcastSearch() respectivamente, e informamos o identificador do grupo ou lista “id” ao invés do número de telefone do destinatário — também usando o método SetDestiny()

Método FileSend()

Para envio de uma mensagem com arquivo anexo, devemos preencher todas as propriedades obrigatórias para envio de uma mensagem — destinatário e texto da mensagem — e informar através do método SetFile() o tipo de arquivo a ser enviado — PDF, JPG, PNG, etc — , o nome do arquivo que deve aparecer na mensagem, e o conteúdo binário do arquivo, codificado em BASE64. Por exemplo, para enviar o arquivo chamado “\filetosend.pdf” que está salvo em disco no seu ambiente a partir do rootpath para um destinatário, que deve receber esse arquivo com o nome ‘meucomprovante.pdf’, usamos por exemplo:

oNetIZap:SetFile("meucomprovante.pdf","PDF",Encode64(MemoRead('\filetosend.pdf')))

Repare que usamos a função MemoRead() para ler o arquivo no disco e retornar seu conteúdo. Se esquecermos de ler o arquivo e colocarmos como parâmetro apenas o nome do arquivo, o destinatário vai receber um arquivo com a string “\filetosend.pdf” como conteúdo 😛

Método QuestionSend()

Podemos enviar uma mensagem que deve ser respondida pelo destinatário. Uma mensagem com pergunta deve conter uma informação chamada  “Question” para envio, onde informamos as respostas esperadas da pergunta. Neste caso a API envia a mensagem ao destinatário, e entende que a próxima mensagem que o destinatário enviar será interpretada como a resposta da pergunta enviada. O texto enviado como resposta sempre será recebido pela API, mesmo que não seja uma das alternativas informadas na pergunta. O método retorna um protocolo de mensagem, mas para saber se ou o que foi respondido para uma pergunta, devemos usar o método QuestionSearch(). Se usarmos o método MessageSearch() com um protocolo de uma pergunta enviada pelo QuestionSend(), a API retorna um erro dizendo que não encontrou a mensagem.

Método MessageSearch()

Usando um número de protocolo de mensagem enviada — obtido no retorno do método MessageSend() ou FileSend() — podemos verificar se a mensagem foi enviada com sucesso ao destinatário, como vimos no exemplo mais acima. É necessário apenas informar o número de protocolo da mensagem, retornado pelo envio de mensagens feitos com MessageSend() e/ou FileSend(). Segue abaixo exemplo de retorno:

{
 "reference":"",
 "sent":"true",
 "sent_datehour":"2019-12-03T13:30:46",
 "destiny":"5511*********",
 "error":"false"
}

Método QuestionSearch()

Uma mensagem com pergunta enviada pelo método QuestionSend() pode ser consultada usando o método QuestionSearch(). Veja abaixo um exemplo de retorno de uma pergunta respondida com uma das alternativas informadas na pergunta:

{"reference":"",
 "sent":"true",
 "sent_datehour":"2019-12-03T22:59:42",
 "destiny":"5511*********",
 "error":"false",
 "question_answer":"true",
 "question_answer_correct":"true",
 "question_response":"2",
 "question_expired":"false"}

Método GroupSearch()

Este método não exige parâmetros adicionais, e retorna um JSON contendo os grupos de mensagens associados ao plano / conta atuais, vide exemplo de retorno abaixo:

 {"root":
  [
   {"id":"5527998022075-1565662333@g.us",
    "descricao":"My Group Example"},
   {"id":"5527988543306-1574680080@g.us",
    "descricao":"Teste API"}
  ]
}

O “id” da lista retornada pode ser usado como destinatário — SetDDestiny() — de uma mensagem, arquivo ou pergunta.

Método BroadcastSearch()

Este método não exige parâmetros adicionais, e retorna um JSON contendo os grupos de mensagens associados ao plano / conta atuais, vide exemplo de retorno abaixo:

{"root":
  [
    {"id":"1573267020@broadcast","descricao":"My Broadcast Example"}
  ]
}

O “id” do grupo retornado pode ser usado como destinatário — SetDDestiny() — de uma mensagem, arquivo ou pergunta.

Método RequestsStart()

Este método não exige parâmetros, e retorna um JSON com um resumo das mensagens enviadas pelo plano atual, agrupando por destinatário. Vide abaixo um exemplo de retorno:

{
  "root": [
    {
      "phone": "55119********",
      "message_datehour_first": "2019-12-04T00:27:22",
      "message_datehour_last": "2019-12-04T00:27:22",
      "messages": [
        {
          "id": "8651",
          "datehour": "2019-12-04T00:27:22",
          "message": "1"
        }
      ],
       "messages_count": 1
     }
  ]
}

Conclusão

Se usa protocolo sobre TCP, recebe parâmetros e tem retorno, dá pra usar em AdvPL 😛 Aguardem novidades no próximo post sobre esse assunto, estou montando uma aplicação de exemplo em AdvPL com interface — SmartClient — para consumir essas APIs de modo interativo 😀

Agradeço novamente a audiência e lhes desejo TERABYTES DE SUCESSO 😀 

Referências

A API foi escrita com base na documentação disponível no link PostMan – NETiZAP Messages API, bem como nos exemplos de conexão publicados no post “SQL Server – Como enviar mensagens para contatos, grupos e listas de transmissão do Whatsapp via API“, do Blog do DBA Dirceu Resende, que inclusive tem mais informações, e um link de Download de uma aplicação de exemplo fornecida pelo provedor 😀

 

JSON – O que é e como usar em AdvPL – Parte 02

Introdução

No post anterior  — JSON – O que é e como usar em AdvPL — vimos o que é JSON e como fazemos para ler um objeto / arquivo JSON em AdvPL ! Agora, vamos ver como criar e editar um objeto JSON em AdvPL 😀

Criando e/ou editando um JSON em AdvPL

Tão simples quanto ler, é criar ou editar um JSON ! Sim, é isso mesmo. A sintaxe direta de endereçar uma propriedade do Objeto JSON em AdvPL — vista no primeiro post — usando a sintaxe de “array” para endereçar propriedades ( oJson[‘propriedade’] ) também serve para fazer uma atribuição. Veja o exemplo abaixo, criando um objeto JSON em AdvPL e depois obtendo a string que o representa:

User Function JsonNew()

oJson := JSonObject():New()

oJson['Id'] := 0984
oJson['Nome'] := 'Júlio Wittwer'
oJson['Ok'] := .T.
oJson['Aniversario'] := ctod('03/03/1976')

oJson['Outros'] := JSonObject():New()
oJson['Outros']['Sangue'] := 'A+'
oJson['Outros']['Eleitor'] := .T.
oJson['Outros']['Reservista'] := .T.

conout(oJson:ToJson())

Return

Resultado obtido no console:

{“Outros”:{“Eleitor”:true,”Reservista”:true,”Sangue”:”A+”},”Id”:984,”Nome”:”Júlio Wittwer”,”Ok”:true,”Aniversario”:”1976/03/03″}

Ao criar um objeto, cada propriedade que receba uma atribuição é criada dinamicamente. Para criar um objeto como uma propriedade, usamos o construtor JsonObject():New(). Ao criarmos uma propriedade contento uma data em AdvPL, ela será criada no JSON como uma String, no formato AAAA/MM/DD. Os valores .T. (verdadeiro) e .F. (falso) em AdvPL viram respectivamente true e false no JSON. Podemos gerar a representação em texto/string do JSON usando o método toJson(). Consulte a documentação oficial do TDN, existe um método ( JsonObject:Set() ) que permite setar um array diretamente a um objeto JSON.

E para remover uma propriedade ?

A funcionalidade de remover uma propriedade de um objeto JSON é fornecida pelo método DelName (TDN – Classe JSonObject), disponível no APPServer a partir da build 17.3.019 😁

Demais considerações

Podemos trabalhar com objetos JSON inclusive maiores que 1MB — desde que você configure o MaxStringSize do Application Server para um novo limite — e com um desempenho muito bom em AdvPL. As sintaxes de acesso direto a propriedades permitem uma abordagem elegante, ágil e simples.

Conclusão

Espero que este conhecimento lhe seja útil, agradeço novamente pela audiência, e desejo a todos TERABYTES DE SUCESSO !!! ❤ Não deixe de consultar os links de referência no final do post. 

Referências

Seu primeiro Webservice REST no ERP Protheus

Desmistificando o MVC no AdvPL

  – O que é REST?

A web é amplamente utilizada e reconhecida principalmente por sua arquitetura robusta, escalável e tolerante a falhas. Quem sustenta esses fatores e lhe dá todo este poder é o protocolo HTTP (o protocolo HTTP é utilizado, em regra, quando se deseja evitar que a informação transmitida entre o cliente e o servidor seja visualizada por terceiros, como, por exemplo, no caso de compras online.). Atualmente, muitas vezes necessitamos integrar aplicações em ambientes totalmente diferentes e os WebServices são uma das maneiras mais comuns e fáceis de integrar os diferentes sistemas. Este post mostrará um pouco de um modelo de WebService chamado REST.

Representational State Transfer ou somente REST, é cada vez mais usado como alternativa ao “já antigo” SOAP onde que a principal crítica a esta é a burocracia, algo que REST possui em uma escala muito menor.
REST é baseado no design do protocolo HTTP, que já possui diversos mecanismos…

Ver o post original 1.454 mais palavras

Web Services em AdvPL – Parte 02

Introdução

No tópico anterior sobre Web Services em AdvPL, foi apresentado um overview dos Web Services, e dois exemplos de código AdvPL para atuar como client de Web Services, utilizando um serviço publico na Internet para previsão do tempo.

Hoje, vamos criar um exemplo de um Web Service SERVER em AdvPL, que receberá como parâmetro uma estrutura complexa (ComplexType), e dentro dele um array de estruturas complexas. A idéia inicial era criar um exemplo completo de envio e retorno, mas o tópico ficaria muito grande, então optei por dividí-lo.

Web Service SERVER em AdvPL

Existe um tópico na TDN, que engloba todos os aspectos de criação de um Web Services SERVER em AdvPL, desde a configuração até o deploy de um serviço. Ele está disponível no link http://tdn.totvs.com/pages/viewpage.action?pageId=6064936 . O conteúdo do post atual no blog complementa algumas definições e parte para um exemplo onde é recomendável que você já tenha lido a TDN.

Resumindo em poucas linhas, um Web Service SERVER em AdvPL consiste na declaração de uma classe diferenciada (WSSERVICE), onde configuramos uma interface de publicação para Web Services no TOTVS Application Server, que a partir das classes WSSERVICE compiladas no RPO, consegue disponibilizar uma interface de chamada, consulta e testes via HTTP, bem como a definição (WSDL) da classe para que outras linguagens que trabalham nativamente com Web Services sejam capazes de gerar uma classe Client para consumir este Web Service.

Parâmetros e retornos

Na declaração de um Web Services SERVER em AdvPL, os parâmetros e retornos dos métodos devem ser declarados como propriedades da classe. O tipo destes parâmetros podem ser tipos simples ( http://tdn.totvs.com/pages/viewpage.action?pageId=6064941 ) ou tipos complexos ( http://tdn.totvs.com/display/tec/05.+Estruturas+-+Tipos+complexos ). Quando a quantidade de elementos de um parametro (simples ou compexo) for maior que 1, podemos declarar a propriedade como um ARRAY OF , onde o tipo pode ser simples ou complexo.

Na prática, um tipo complexo não passa de um agrupamento de um ou mais tipos simples ou complexos, como se fosse uma estrutura (classe sem métodos). Em um Web Service AdvPL, é possível declarar metodos para as estruturas, mas os métodos das estruturas não são publicados no WSDL, podem ser usados apenas dentro da classe de Web Service SERVER.

Projeto Web Services TORNEIOS

Vamos partir de um exemplo didático da criação de um WebServices para cadastrar um torneio esportivo. Os dados relevantes do torneio são as datas de inicio e final, uma descrição, e uma lista de atletas participantes. Cada atleta deve informar seu CPF, nome e data de nascimento. Opcionalmente, o atleta pode informar seu peso (em Kg) e altura (em metros).

Logo, precisamos de uma estrutura para os dados de um torneio e outra para os dados do atleta. Como um torneio pode ter vários atletas, a estrutura do atleta deve ser declarada como uma propriedade do tipo ARRAY OF ATLETA na estrutura de torneios. Vamos à primeira parte do código:

( Fonte WSSRV01.PRW )

#include "protheus.ch"
#include "apwebsrv.ch"
/* -------------------------------------------------------------
Fonte WSSRV01.PRW 
Autor Julio Wittwer
Data 10/06/2015
Descrição Fonte de exemplo de Web Services Server com 
 recebimento e retorno de tipos complexos 
 e array de tipos complexos
------------------------------------------------------------- */
// Estutura de dados de um atleta
// CPF, Nome, Aniversário ( obrigatorios ) 
// peso e altura ( opcionais )
WSSTRUCT ATLETA
 
 WSDATA CPF as STRING
 WSDATA Nome as STRING
 WSDATA Nascim as DATE 
 WSDATA PesoKg as FLOAT OPTIONAL
 WSDATA Altura as FLOAT OPTIONAL
ENDWSSTRUCT
// Estrutura de dados de um torneio 
// Descrição, data de inicio e término, e array de atletas participantes
WSSTRUCt TORNEIO
WSDATA Descricao as STRING
 WSDATA Inicio as DATE
 WSDATA Final as DATE 
 WSDATA Atletas as ARRAY OF ATLETA
 
ENDWSSTRUCT

Todas as propriedades declaradas nas estruturas (WSDATA) por default são obrigatórias. Para tornar uma propriedade opcional, usamos a palavra reservada “OPTIONAL” após o tipo da propriedade. Feita a declaração das estruturas, vamos agora para a prototipação da classe.

// Prototipação do WebServices
// Serviço de cadastro de torneios esportivos
WSSERVICE TORNEIOS DESCRIPTION "Torneios Esportivos"
 
 // propriedades de entrada e retorno, usados 
 // como parametros ou retornos nos metodos
 
 WSDATA TorneioIn AS TORNEIO 
 WSDATA Status as INTEGER 
 
 // Metodos do WebService
 WSMETHOD Incluir
ENDWSSERVICE

Por enquanto o WebService vai apenas receber os dados completos de um torneio. O foco do exemplo é justamente como os dados serão recebidos, e como você poderá enviá-los através de um Web Service client em AdvPL criado pela geração de classe client do TDS ou pela nova classe tWSDLManager. O exemplo não abrange a persistência das informações em base de dados. Agora, vamos implementar o método “Incluir” neste fonte:

// Metodo de inclusao. Recebe um Torneio com todos os dados 
// e o array de atletas, e retorna um status numerico ( 0 = sucesso )
WSMETHOD Incluir WSRECEIVE TorneioIn WSSEND Status WSSERVICE TORNEIOS 
 
// Vamos ver tudo o que chegou como parametro
// varinfo("TorneioIn",::TorneioIn) 
 
/*
TorneioIn -> OBJECT ( 4) [...]
 TorneioIn:ATLETAS -> ARRAY ( 2) [...]
 TorneioIn:ATLETAS[1] -> OBJECT ( 5) [...]
 TorneioIn:ATLETAS[1]:ALTURA -> U ( 1) [ ]
 TorneioIn:ATLETAS[1]:CPF -> C ( 12) [123456789-09]
 TorneioIn:ATLETAS[1]:NASCIM -> D ( 8) [02/02/81]
 TorneioIn:ATLETAS[1]:NOME -> C ( 13) [Joao da Silva]
 TorneioIn:ATLETAS[1]:PESOKG -> N ( 15) [ 72.5000]
 TorneioIn:ATLETAS[2] -> OBJECT ( 5) [...]
 TorneioIn:ATLETAS[2]:ALTURA -> U ( 1) [ ]
 TorneioIn:ATLETAS[2]:CPF -> C ( 12) [111111111-11]
 TorneioIn:ATLETAS[2]:NASCIM -> D ( 8) [03/03/80]
 TorneioIn:ATLETAS[2]:NOME -> C ( 13) [Jose da Silva]
 TorneioIn:ATLETAS[2]:PESOKG -> N ( 15) [ 75.2000]
 TorneioIn:DESCRICAO -> C ( 16) [Torneio de Teste]
 TorneioIn:FINAL -> D ( 8) [06/29/15]
 TorneioIn:INICIO -> D ( 8) [06/28/15]
*/
 
// com isso conseguimos montar facilmente o codigo AdvPl para tratar os dados 
// recebidos nesta propriedade. Logo abaixo, eu mostro na tela de console do 
// TOTVS App server a descricao do torneio recebida, e o nome dos atletas recebidos
conout("Torneio : "+TorneioIn:DESCRICAO)
conout("Atletas : "+cValToChar( len(TorneioIn:ATLETAS) ))
For nI := 1 to len(TorneioIn:ATLETAS)
 conout(TorneioIn:ATLETAS[nI]:Nome)
Next
// Caso o processamento seja concluido com sucesso, 
// alimentamos a propriedade de retorno com 0
// em caso de falha, podemos usar um ou mais numeros negativos para indicar
// uma impossibilidade de processamento, como um torneio na mesma data....
::Status := 0
Return .T.

Uma vez criado o fonte e compilado no RPO, e configurado adequadamente o APPServer para ser um servidor de HTTP para WEB Services (vide documentação da TDN), podemos partir para a geração do fonte client em AdvPL.

Exemplo gerando o fonte client de Web Services

Primeiro, vamos gerar um fonte client AdvPL usando o IDE ou TDS. Usando o endereço HTTP onde foi disponibilizado o WSDL deste serviço, o código fonte client gerado deve ficar assim:

( Fonte WSCLI01.PRW )

#INCLUDE "PROTHEUS.CH"
#INCLUDE "APWEBSRV.CH"
/* ===============================================================================
WSDL Location http://localhost/ws/TORNEIOS.apw?WSDL
Gerado em 06/28/15 15:31:26
Observações Código-Fonte gerado por ADVPL WSDL Client 1.120703
 Alterações neste arquivo podem causar funcionamento incorreto
 e serão perdidas caso o código-fonte seja gerado novamente.
=============================================================================== */
User Function _SSLXTLW ; Return // "dummy" function - Internal Use
/* -------------------------------------------------------------------------------
WSDL Service WSTORNEIOS
------------------------------------------------------------------------------- */
WSCLIENT WSTORNEIOS
WSMETHOD NEW
 WSMETHOD INIT
 WSMETHOD RESET
 WSMETHOD CLONE
 WSMETHOD INCLUIR
WSDATA _URL AS String
 WSDATA _HEADOUT AS Array of String
 WSDATA _COOKIES AS Array of String
 WSDATA oWSTORNEIOIN AS TORNEIOS_TORNEIO
 WSDATA nINCLUIRRESULT AS integer
// Estruturas mantidas por compatibilidade - NÃO USAR
 WSDATA oWSTORNEIO AS TORNEIOS_TORNEIO
ENDWSCLIENT
WSMETHOD NEW WSCLIENT WSTORNEIOS
::Init()
If !FindFunction("XMLCHILDEX")
 UserException("O Código-Fonte Client atual requer os executáveis do Protheus Build [7.00.131227A-20150620] ou superior. Atualize o Protheus ou gere o Código-Fonte novamente utilizando o Build atual.")
EndIf
Return Self
WSMETHOD INIT WSCLIENT WSTORNEIOS
 ::oWSTORNEIOIN := TORNEIOS_TORNEIO():New()
// Estruturas mantidas por compatibilidade - NÃO USAR
 ::oWSTORNEIO := ::oWSTORNEIOIN
Return
WSMETHOD RESET WSCLIENT WSTORNEIOS
 ::oWSTORNEIOIN := NIL 
 ::nINCLUIRRESULT := NIL
// Estruturas mantidas por compatibilidade - NÃO USAR
 ::oWSTORNEIO := NIL
 ::Init()
Return
WSMETHOD CLONE WSCLIENT WSTORNEIOS
Local oClone := WSTORNEIOS():New()
 oClone:_URL := ::_URL 
 oClone:oWSTORNEIOIN := IIF(::oWSTORNEIOIN = NIL , NIL ,::oWSTORNEIOIN:Clone() )
 oClone:nINCLUIRRESULT := ::nINCLUIRRESULT
// Estruturas mantidas por compatibilidade - NÃO USAR
 oClone:oWSTORNEIO := oClone:oWSTORNEIOIN
Return oClone
// WSDL Method INCLUIR of Service WSTORNEIOS
WSMETHOD INCLUIR WSSEND oWSTORNEIOIN WSRECEIVE nINCLUIRRESULT WSCLIENT WSTORNEIOS
Local cSoap := "" , oXmlRet
BEGIN WSMETHOD
cSoap += 'xmlns="http://localhost/">'
cSoap += WSSoapValue("TORNEIOIN", ::oWSTORNEIOIN, oWSTORNEIOIN , "TORNEIO", .T. , .F., 0 , NIL, .F.) 
cSoap += "</INCLUIR>"
oXmlRet := SvcSoapCall( Self,cSoap,; 
 "http://localhost/INCLUIR",; 
 "DOCUMENT","http://localhost/",,"1.031217",; 
 "http://localhost/ws/TORNEIOS.apw")
::Init()
::nINCLUIRRESULT := WSAdvValue( oXmlRet,"_INCLUIRRESPONSE:_INCLUIRRESULT:TEXT","integer",NIL,NIL,NIL,NIL,NIL,NIL)
END WSMETHOD
oXmlRet := NIL
Return .T.
// WSDL Data Structure TORNEIO
WSSTRUCT TORNEIOS_TORNEIO
 WSDATA oWSATLETAS AS TORNEIOS_ARRAYOFATLETA
 WSDATA cDESCRICAO AS string
 WSDATA dFINAL AS date
 WSDATA dINICIO AS date
 WSMETHOD NEW
 WSMETHOD INIT
 WSMETHOD CLONE
 WSMETHOD SOAPSEND
 WSMETHOD SOAPRECV
ENDWSSTRUCT
WSMETHOD NEW WSCLIENT TORNEIOS_TORNEIO
 ::Init()
Return Self
WSMETHOD INIT WSCLIENT TORNEIOS_TORNEIO
Return
WSMETHOD CLONE WSCLIENT TORNEIOS_TORNEIO
 Local oClone := TORNEIOS_TORNEIO():NEW()
 oClone:oWSATLETAS := IIF(::oWSATLETAS = NIL , NIL , ::oWSATLETAS:Clone() )
 oClone:cDESCRICAO := ::cDESCRICAO
 oClone:dFINAL := ::dFINAL
 oClone:dINICIO := ::dINICIO
Return oClone
WSMETHOD SOAPSEND WSCLIENT TORNEIOS_TORNEIO
 Local cSoap := ""
 cSoap += WSSoapValue("ATLETAS", ::oWSATLETAS, ::oWSATLETAS , "ARRAYOFATLETA", .T. , .F., 0 , NIL, .F.) 
 cSoap += WSSoapValue("DESCRICAO", ::cDESCRICAO, ::cDESCRICAO , "string", .T. , .F., 0 , NIL, .F.) 
 cSoap += WSSoapValue("FINAL", ::dFINAL, ::dFINAL , "date", .T. , .F., 0 , NIL, .F.) 
 cSoap += WSSoapValue("INICIO", ::dINICIO, ::dINICIO , "date", .T. , .F., 0 , NIL, .F.) 
Return cSoap
WSMETHOD SOAPRECV WSSEND oResponse WSCLIENT TORNEIOS_TORNEIO
 Local oNode1
 ::Init()
 If oResponse = NIL ; Return ; Endif 
 oNode1 := WSAdvValue( oResponse,"_ATLETAS","ARRAYOFATLETA",NIL,"Property oWSATLETAS as s0:ARRAYOFATLETA on SOAP Response not found.",NIL,"O",NIL,NIL) 
 If oNode1 != NIL
 ::oWSATLETAS := TORNEIOS_ARRAYOFATLETA():New()
 ::oWSATLETAS:SoapRecv(oNode1)
 EndIf
 ::cDESCRICAO := WSAdvValue( oResponse,"_DESCRICAO","string",NIL,"Property cDESCRICAO as s:string on SOAP Response not found.",NIL,"S",NIL,NIL) 
 ::dFINAL := WSAdvValue( oResponse,"_FINAL","date",NIL,"Property dFINAL as s:date on SOAP Response not found.",NIL,"D",NIL,NIL) 
 ::dINICIO := WSAdvValue( oResponse,"_INICIO","date",NIL,"Property dINICIO as s:date on SOAP Response not found.",NIL,"D",NIL,NIL) 
Return
// WSDL Data Structure ARRAYOFATLETA
WSSTRUCT TORNEIOS_ARRAYOFATLETA
 WSDATA oWSATLETA AS TORNEIOS_ATLETA OPTIONAL
 WSMETHOD NEW
 WSMETHOD INIT
 WSMETHOD CLONE
 WSMETHOD SOAPSEND
 WSMETHOD SOAPRECV
ENDWSSTRUCT
WSMETHOD NEW WSCLIENT TORNEIOS_ARRAYOFATLETA
 ::Init()
Return Self
WSMETHOD INIT WSCLIENT TORNEIOS_ARRAYOFATLETA
 ::oWSATLETA := {} // Array Of TORNEIOS_ATLETA():New()
Return
WSMETHOD CLONE WSCLIENT TORNEIOS_ARRAYOFATLETA
 Local oClone := TORNEIOS_ARRAYOFATLETA():NEW()
 oClone:oWSATLETA := NIL
 If ::oWSATLETA <> NIL 
 oClone:oWSATLETA := {}
 aEval( ::oWSATLETA , { |x| aadd( oClone:oWSATLETA , x:Clone() ) } )
 Endif 
Return oClone
WSMETHOD SOAPSEND WSCLIENT TORNEIOS_ARRAYOFATLETA
 Local cSoap := ""
 aEval( ::oWSATLETA , {|x| cSoap := cSoap + WSSoapValue("ATLETA", x , x , "ATLETA", .F. , .F., 0 , NIL, .F.) } ) 
Return cSoap
WSMETHOD SOAPRECV WSSEND oResponse WSCLIENT TORNEIOS_ARRAYOFATLETA
 Local nRElem1, oNodes1, nTElem1
 ::Init()
 If oResponse = NIL ; Return ; Endif 
 oNodes1 := WSAdvValue( oResponse,"_ATLETA","ATLETA",{},NIL,.T.,"O",NIL,NIL) 
 nTElem1 := len(oNodes1)
 For nRElem1 := 1 to nTElem1 
 If !WSIsNilNode( oNodes1[nRElem1] )
 aadd(::oWSATLETA , TORNEIOS_ATLETA():New() )
 ::oWSATLETA[len(::oWSATLETA)]:SoapRecv(oNodes1[nRElem1])
 Endif
 Next
Return
// WSDL Data Structure ATLETA
WSSTRUCT TORNEIOS_ATLETA
 WSDATA nALTURA AS float OPTIONAL
 WSDATA cCPF AS string
 WSDATA dNASCIM AS date
 WSDATA cNOME AS string
 WSDATA nPESOKG AS float OPTIONAL
 WSMETHOD NEW
 WSMETHOD INIT
 WSMETHOD CLONE
 WSMETHOD SOAPSEND
 WSMETHOD SOAPRECV
ENDWSSTRUCT
WSMETHOD NEW WSCLIENT TORNEIOS_ATLETA
 ::Init()
Return Self
WSMETHOD INIT WSCLIENT TORNEIOS_ATLETA
Return
WSMETHOD CLONE WSCLIENT TORNEIOS_ATLETA
 Local oClone := TORNEIOS_ATLETA():NEW()
 oClone:nALTURA := ::nALTURA
 oClone:cCPF := ::cCPF
 oClone:dNASCIM := ::dNASCIM
 oClone:cNOME := ::cNOME
 oClone:nPESOKG := ::nPESOKG
Return oClone
WSMETHOD SOAPSEND WSCLIENT TORNEIOS_ATLETA
 Local cSoap := ""
 cSoap += WSSoapValue("ALTURA", ::nALTURA, ::nALTURA , "float", .F. , .F., 0 , NIL, .F.) 
 cSoap += WSSoapValue("CPF", ::cCPF, ::cCPF , "string", .T. , .F., 0 , NIL, .F.) 
 cSoap += WSSoapValue("NASCIM", ::dNASCIM, ::dNASCIM , "date", .T. , .F., 0 , NIL, .F.) 
 cSoap += WSSoapValue("NOME", ::cNOME, ::cNOME , "string", .T. , .F., 0 , NIL, .F.) 
 cSoap += WSSoapValue("PESOKG", ::nPESOKG, ::nPESOKG , "float", .F. , .F., 0 , NIL, .F.) 
Return cSoap
WSMETHOD SOAPRECV WSSEND oResponse WSCLIENT TORNEIOS_ATLETA
 ::Init()
 If oResponse = NIL ; Return ; Endif 
 ::nALTURA := WSAdvValue( oResponse,"_ALTURA","float",NIL,NIL,NIL,"N",NIL,NIL) 
 ::cCPF := WSAdvValue( oResponse,"_CPF","string",NIL,"Property cCPF as s:string on SOAP Response not found.",NIL,"S",NIL,NIL) 
 ::dNASCIM := WSAdvValue( oResponse,"_NASCIM","date",NIL,"Property dNASCIM as s:date on SOAP Response not found.",NIL,"D",NIL,NIL) 
 ::cNOME := WSAdvValue( oResponse,"_NOME","string",NIL,"Property cNOME as s:string on SOAP Response not found.",NIL,"S",NIL,NIL) 
 ::nPESOKG := WSAdvValue( oResponse,"_PESOKG","float",NIL,NIL,NIL,"N",NIL,NIL) 
Return

Agora, podemos montar um novo fonte ( WSTST01.PRW ), que vai usar a classe client gerada no AdvPL para interagir com este serviço. No fonte abaixo, todos os detalhes da implementação e o passo a passo utilizado para entender como usar as estruturas e métodos definidos na classe client do Web Service.

#include "protheus.ch"
/* ===========================================================================
Fonte WSTST01.PRW
Funcoes U_TST01I()
Autor Julio Wittwer
Data 10/06/2015
Descricao Fonte de testes da classe client do WebService "TORNEIOS" em AddPl
=========================================================================== */
User Function Tst01I()
Local oWSClient 
Local oAtleta
SET DATE BRITISH
SET CENTURY ON 
SET EPOCH TO 1960
 
// Cria a instância da classe Client do WebService de Torneios
oWSClient := WSTORNEIOS():New()
/*
Vamos chamar a inclusao de torneios. para isso, primeiro vemos qual foi o metodo 
de inclusao gerado na classe client Advpl
WSMETHOD INCLUIR WSSEND oWSTORNEIOIN WSRECEIVE nINCLUIRRESULT WSCLIENT WSTORNEIOS
*/
// o parametro oWSTorneioIn é uma estrutura nomeada de TORNEIOS_TORNEIO
// Como esta estrutura é um parâmetro de um método, ela já vem 
// criada, mas seus valores nao estao preenchidos 
// vamos preencher as propriedades desta estrutura
// consultamos as propriedades da estrutura no fonte Client Advpl gerado
/*
WSSTRUCT TORNEIOS_TORNEIO
 WSDATA oWSATLETAS AS TORNEIOS_ARRAYOFATLETA
 WSDATA cDESCRICAO AS string
 WSDATA dFINAL AS date
 WSDATA dINICIO AS date
(...)
*/
oWSClient:oWSTORNEIOIN:cDESCRICAO := 'Torneio de Teste'
oWSClient:oWSTORNEIOIN:dInicio := date()+1
oWSClient:oWSTORNEIOIN:dFinal := date()+2
// Dentro da estrutura de torneio, temos tambem a propriedade oWSAtletas
// Como ela nao vem criada, vamos criar uma instância dela
oWSClient:oWSTORNEIOIN:oWSATLETAS := TORNEIOS_ARRAYOFATLETA():New()
// Como a estrutura é um encapsulamento de array de objetos, 
// a propriedade oWSATLETA desta estrutura vem com {} um array vazio 
// ::oWSATLETA := {} // Array Of TORNEIOS_ATLETA():New()
// Para cada atleta a acrescentar, criamos uma nova instancia da estrutura 
// TORNEIOS_ATLETA . Devemos ver a declaração da estrutura para saber 
// quais propriedades devem ser preenchidas
 
/*
WSSTRUCT TORNEIOS_ATLETA
 WSDATA nALTURA AS float OPTIONAL
 WSDATA cCPF AS string
 WSDATA dNASCIM AS date
 WSDATA cNOME AS string
 WSDATA nPESOKG AS float OPTIONAL
(...)
*/
oAtleta := TORNEIOS_ATLETA():New()
oAtleta:cNome := "Joao da Silva"
oAtleta:cCPF := "123456789-09"
oAtleta:dNASCIM := ctod('02/02/1981')
oAtleta:nPESOKG := 72.5
// agora acrescentamos o atleta da propriedade correta
aadd( oWSClient:oWSTORNEIOIN:oWSATLETAS:oWSATLETA , oAtleta ) 
 
// Criamos um novo atleta
oAtleta := TORNEIOS_ATLETA():New()
oAtleta:cNome := "Jose da Silva"
oAtleta:cCPF := "111111111-11"
oAtleta:dNASCIM := ctod('03/03/1980')
oAtleta:nPESOKG := 75.2
// agora acrescentamos o segundo atleta da propriedade correta
aadd( oWSClient:oWSTORNEIOIN:oWSATLETAS:oWSATLETA , oAtleta )
// vamos ver como ficou a estrutura 
// varinfo("oWSClient:oWSTORNEIOIN",oWSClient:oWSTORNEIOIN)
// agora vamos chamar o metodo de inclusao
If oWSClient:Incluir() 
 
 // Chamada realizada com sucesso. 
 // Vamos ver o que chegou na propriedade de retorno
 msgInfo("Codigo de retorno = "+cValToChar(oWSClient:nINCLUIRRESULT),"Chamada realizada.")
Else
// houve algum erro na chamada
 // ou no processamento do webservice 
 MsgStop(getwscerror(3))
Endif
Return

A função de testes de inclusão ( U_TST01I ) pode ser chamada diretamente pelo SmartClient, não precisa colocar ela dentro do Menu do ERP. Os dados utilizados para os atletas estão “chumbados” no fonte. Ao ser executado, o Web Services SERVER deve receber e aceitar o XML enviado por HTTP, e mostrar no LOG de console a descrição do torneio e o nome dos dois atletas informados.

Exemplo usando tWSDLManager

Agora sim, vamos ver como é possível usar a casse nova de Web Services para informar estruturas complexas, e arrays de estruturas complexas.

A classe tWSDLManager permite mapear, a partir do WSDL, as estruturas complexas e tipos simples que precisam ser enviados para uma requisição de WebServices. Como um tipo complexo na verdade é um agrupamento de tipos simples, ao usar o método SimpleInput(), vamos obter um array contendo os identificadores de todos os dados que podem ser enviados, porem a classe considera por default que voce vai enviar apenas uma ocorrencia de um dado ou estrutura. Como nós sabemos que vamos inserir mais de um valor, precisamos dizer para a classe quantos valores serão informados, e somente com estes valores definidos podemos usar o método SimpleInput() para ver como e onde os dados serão informados. Toda a sequencia de identificação está documentada no proprio fonte, em linhas de comentário.

#include "protheus.ch"
/* ===========================================================================
Fonte WSTST02.PRW
Funcoes U_TST02I()
Autor Julio Wittwer
Data 28/06/2015
Descricao Fonte de testes do webService de Torneios, usando a nova classe
 client de websservices "tWSDLManager"
http://tdn.totvs.com/display/tec/Classe+TWsdlManager
=========================================================================== */
User Function TST02I()
Local oWSDL
Local lOk, cResp, aElem, nPos
oWSDL := tWSDLManager():New()
// Seta o modo de trabalho da classe para "verbose"
// apenas para fins de desenvolvimento e depuracao
// oWSDL:lVerbose := .T.
// Primeiro faz o parser do WSDL a partir da URL
lOk := oWsdl:ParseURL( "http://localhost/ws/TORNEIOS.apw?WSDL" )
if !lOk
 MsgStop( oWsdl:cError , "ParseURL() ERROR")
 Return
endif
// Lista as operações disponíveis
// aOps := oWsdl:ListOperations()
// varinfo( "aOps", aOps )
/*
aOps -> ARRAY ( 1) [...]
aOps[1] -> ARRAY ( 2) [...]
aOps[1][1] -> C ( 7) [INCLUIR]
aOps[1][2] -> C ( 0) []
*/
// Seta a operação a ser utilizada
lOk := oWsdl:SetOperation( "INCLUIR" )
if !lOk
 MsgStop( oWsdl:cError , "SetOperation(INCLUIR) ERROR")
 Return
endif
// Agora vamos ver os parametros de chamada da inclusao
// primeiro, devemos verificar os complex types de
// tamanho variavel.
// aParC := oWsdl:ComplexInput()
// varinfo("aParC",aParC)
/*
aParC -> ARRAY ( 1) [...]
aParC[1] -> ARRAY ( 5) [...]
aParC[1][1] -> N ( 15) [ 3.0000] // ID da estrutura complexa variavel
aParC[1][2] -> C ( 6) [ATLETA]
aParC[1][3] -> N ( 15) [ 0.0000] // numero minimo de ocorrencias
aParC[1][4] -> N ( 15) [2147483647.0000] // numero maximo de ocorrencias
aParC[1][5] -> C ( 31) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1] // lista de elementos "pai" desta estrutura
*/
// Como sabemos que vamos enviar 2 atletas,
// definimos esta quantidade usando o metodo SetComplexOccurs
oWSDL:SetComplexOccurs(3,2) // Id do complex type, quantidade a ser enviada
// Apenas depois de definirmos quantos tipos complexos queremos enviar,
// podemos listar os tipos simples.
// aParS := oWsdl:SimpleInput()
// varinfo("aParS",aParS)
/*
aParS -> ARRAY ( 13) [...]
aParS[1] -> ARRAY ( 5) [...]
aParS[1][1] -> N ( 15) [ 0.0000]
aParS[1][2] -> C ( 6) [ALTURA]
aParS[1][3] -> N ( 15) [ 0.0000]
aParS[1][4] -> N ( 15) [ 1.0000]
aParS[1][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#1]
aParS[2] -> ARRAY ( 5) [...]
aParS[2][1] -> N ( 15) [ 1.0000]
aParS[2][2] -> C ( 3) [CPF]
aParS[2][3] -> N ( 15) [ 1.0000]
aParS[2][4] -> N ( 15) [ 1.0000]
aParS[2][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#1]
aParS[3] -> ARRAY ( 5) [...]
aParS[3][1] -> N ( 15) [ 2.0000]
aParS[3][2] -> C ( 6) [NASCIM]
aParS[3][3] -> N ( 15) [ 1.0000]
aParS[3][4] -> N ( 15) [ 1.0000]
aParS[3][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#1]
aParS[4] -> ARRAY ( 5) [...]
aParS[4][1] -> N ( 15) [ 3.0000]
aParS[4][2] -> C ( 4) [NOME]
aParS[4][3] -> N ( 15) [ 1.0000]
aParS[4][4] -> N ( 15) [ 1.0000]
aParS[4][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#1]
aParS[5] -> ARRAY ( 5) [...]
aParS[5][1] -> N ( 15) [ 4.0000]
aParS[5][2] -> C ( 6) [PESOKG]
aParS[5][3] -> N ( 15) [ 0.0000]
aParS[5][4] -> N ( 15) [ 1.0000]
aParS[5][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#1]
aParS[6] -> ARRAY ( 5) [...]
aParS[6][1] -> N ( 15) [ 5.0000]
aParS[6][2] -> C ( 6) [ALTURA]
aParS[6][3] -> N ( 15) [ 0.0000]
aParS[6][4] -> N ( 15) [ 1.0000]
aParS[6][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#2]
aParS[7] -> ARRAY ( 5) [...]
aParS[7][1] -> N ( 15) [ 6.0000]
aParS[7][2] -> C ( 3) [CPF]
aParS[7][3] -> N ( 15) [ 1.0000]
aParS[7][4] -> N ( 15) [ 1.0000]
aParS[7][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#2]
aParS[8] -> ARRAY ( 5) [...]
aParS[8][1] -> N ( 15) [ 7.0000]
aParS[8][2] -> C ( 6) [NASCIM]
aParS[8][3] -> N ( 15) [ 1.0000]
aParS[8][4] -> N ( 15) [ 1.0000]
aParS[8][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#2]
aParS[9] -> ARRAY ( 5) [...]
aParS[9][1] -> N ( 15) [ 8.0000]
aParS[9][2] -> C ( 4) [NOME]
aParS[9][3] -> N ( 15) [ 1.0000]
aParS[9][4] -> N ( 15) [ 1.0000]
aParS[9][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#2]
aParS[10] -> ARRAY ( 5) [...]
aParS[10][1] -> N ( 15) [ 9.0000]
aParS[10][2] -> C ( 6) [PESOKG]
aParS[10][3] -> N ( 15) [ 0.0000]
aParS[10][4] -> N ( 15) [ 1.0000]
aParS[10][5] -> C ( 40) [INCLUIR#1.TORNEIOIN#1.ATLETAS#1.ATLETA#2]
aParS[11] -> ARRAY ( 5) [...]
aParS[11][1] -> N ( 15) [ 10.0000]
aParS[11][2] -> C ( 9) [DESCRICAO]
aParS[11][3] -> N ( 15) [ 1.0000]
aParS[11][4] -> N ( 15) [ 1.0000]
aParS[11][5] -> C ( 21) [INCLUIR#1.TORNEIOIN#1]
aParS[12] -> ARRAY ( 5) [...]
aParS[12][1] -> N ( 15) [ 11.0000]
aParS[12][2] -> C ( 5) [FINAL]
aParS[12][3] -> N ( 15) [ 1.0000]
aParS[12][4] -> N ( 15) [ 1.0000]
aParS[12][5] -> C ( 21) [INCLUIR#1.TORNEIOIN#1]
aParS[13] -> ARRAY ( 5) [...]
aParS[13][1] -> N ( 15) [ 12.0000]
aParS[13][2] -> C ( 6) [INICIO]
aParS[13][3] -> N ( 15) [ 1.0000]
aParS[13][4] -> N ( 15) [ 1.0000]
aParS[13][5] -> C ( 21) [INCLUIR#1.TORNEIOIN#1]
*/
// Analisando o array acima, vemos que os parametros de primeiro nivel
// sao INICIO, FINAL e DESCRICAO. Logo, podemos alimentar estes valores
// diretamente, inclusive pelo nome. Porem precisamos informar tambem o array contendo 
// a sequencia de elementos "pai" de cada valor. Na pratica, pasta pegar a 
// lista de elementos pai identificada no 5o elemento do array de simpleInput
// e criar um array com ele.
lOk := oWsdl:SetValPar("DESCRICAO",{"INCLUIR#1","TORNEIOIN#1"}, "Torneio de Testes")
if !lOk
 MsgStop( oWsdl:cError , procline())
 Return
endif
lOk := oWsdl:SetValPar("INICIO",{"INCLUIR#1","TORNEIOIN#1"}, '20150701')
if !lOk
 MsgStop( oWsdl:cError , procline())
 Return
endif
lOk := oWsdl:SetValPar("FINAL",{"INCLUIR#1","TORNEIOIN#1"}, '20150702')
if !lOk
 MsgStop( oWsdl:cError , procline())
 Return
endif
// Podemos ver tambem que o array de tipos simples foi criado para 2 atletas
// e agora temos IDs para cada um deles. Isto somente aconteceu pois 
// usamos o metodo SetComplexOccurs(). Caso nao tivessemos utilizado 
// este metodo, o array de estruturas simples viria apenas com os 
// identificadores para inserir um altleta.
// Para definir os tipos complexos, uma vez que seus valores
// já estão enumerados na lista de tipos simples, podemos fazer assim :
aParent := {"INCLUIR#1","TORNEIOIN#1","ATLETAS#1","ATLETA#1"}
oWsdl:SetValPar("NOME",aParent,"Romualdo da Silva")
oWsdl:SetValPar("CPF",aParent,"123456789-01")
oWsdl:SetValPar("NASCIM",aParent,"1974-05-08")
oWsdl:SetValPar("PESOKG",aParent,"77,5")
oWsdl:SetValPar("ALTURA",aParent,"1,85")
// Como os atletas são inseridos em sequencia, basta informarmos
// a que alteta a informação se destina, variando o ultimo
// elemento do array dos "pais" de cada informação.
aParent := {"INCLUIR#1","TORNEIOIN#1","ATLETAS#1","ATLETA#2"}
oWsdl:SetValPar("NOME",aParent,"Sinfronio da Silva")
oWsdl:SetValPar("CPF",aParent,"555555555-55")
oWsdl:SetValPar("NASCIM",aParent,"1976-03-12")
oWsdl:SetValPar("PESOKG",aParent,"66,5")
oWsdl:SetValPar("ALTURA",aParent,"1,65")
// Agora executamos a requisição ao WebService
lOk := oWsdl:SendSoapMsg()
if !lOk
 MsgStop( oWsdl:cError , "SendSoapMsg() ERROR")
 Return
endif
// mostra no console o XML Soap devolvido
conout( "--- SOAP RESPONSE ---" , oWsdl:GetSoapResponse() , "-------------------")
// Recupera os elementos de retorno, já parseados
cResp := oWsdl:GetParsedResponse()
/*
Como o retorno é apenas um numero, podemos obtelo diretamente 
parseando a string retornada em cResp
*/
varinfo("cResp",cResp)
Return

Conclusão

A classe tWSDLManager tem uma sequência de operações que precisa ser realizada antes de começar a alimentar as propriedades de envio de dados. Caso existam mais informações que possuem quantidades variáveis, é necessário definir a quantidade de cada uma delas antes de começar a popular os valores para realizar a requisição / chamada do WebService.

Para o próximo post sobre Web Services, eu vou pegar este exemplo didático, e fazer um método de consulta, onde o retorno do Web SErvices server vai ser as informações de um torneio. Esta etapa de tratamento de retorno de tipos complexos é muito simples usando a geração do fonte client em AdvPL, mas é um pouco mais complicada quando usamos a classe tWSDLManager(), e realmente tudo isso de uma vez dentro de apenas uma postagem, ficaria muito grande !

Até a próxima pessoal, e bons Web Services 😀

Web Services em AdvPL – Parte 01

Introdução

Quem não conhece, já ouviu falar de Web Services. Traduzindo em pouquíssimas palavras, Web Services é um padrão de comunicação entre sistemas, onde o provedor ( ou “servidor” ) de um Web Service, possui uma camada de publicação de classes e métodos, descritos em um documento baseado em XML, chamado de WSDL ( Web Services Description Language ), através do qual uma linguagem ou plataforma de desenvolvimento pode criar uma classe ou camada de “proxy” para acessar as funcionalidades descritas no WSDL. Normalmente os Web Services utilizam o protocolo HTTP para trocas de mensagens entre a aplicação cliente e o servidor, e são utilizados entre sistemas que podem ou não ser baseados na mesma plataforma ou tecnologia, e as aplicações client e server podem estar na mesma rede ( intranet ) ou na Internet.

WSDL

A especificação da descrição de um Web Service é bem ampla, ela prevê a descrição detalhada das classes e métodos disponíveis, descrição das estruturas de parâmetros e retornos, até mesmo a documentação textual de cada método pode ser especificada dentro do WSDL. Normalmente uma plataforma de desenvolvimento que trabalha com orientação a objetos e é compatível nativamente com Web Services, provê um tipo de classe diferenciada para permitir a publicação de uma classe implementada no formato nativo da linguagem para ser acessada como um Web Service.

SOAP

Soap é um acrônimo para Simple Object Access Protocol, trata-se de uma especificação para troca de informações estruturadas, baseado em XML. O WSDL fornecido descreve como devem ser os pacotes SOAP de envio e retorno de informações de um Web Service. A especificação das mensagens normalmente são simples, porém a especificação permite criar camadas complexas, chegando até mesmo ao nível de declaração de mensagens com herança e utilização de mais de um arquivo de definições, especificados como “includes” ou “imports” dentro dos WSDL(s).

Vantagens

Quando você usa uma plataforma ou ambiente de desenvolvimento que cria uma classe “proxy” para você consumir um Web Service, o mundo é maravilhoso, o céu é azul, tudo é lindo. Você não precisa saber ou entender o que vai dentro do XML, a classe monta tudo para você, basta alimentar os dados nas estruturas de parâmetro e consumir o serviço, e em caso de sucesso a(s) estrutura(s) de retorno estarão preenchidas. Com o detalhamento do WSDL, a classe proxy gerada pode até já vir documentada. A utilização do protocolo HTTP para envio de requisições torna mais fácil a criação de integrações de sistemas baseados em SOA (Service-Oriented Architecture), ou Arquitetura Orientada a Serviços.

Desvantagens

Quando falamos de transações curtas, requisições pequenas, Web Services é ótimo. Mas se você vai enfiar em uma única requisição um caminhão de dados, ou mesmo arquivos, a natureza de parser de XML — que normalmente exige que o documento inteiro seja parseado ou analisado antes do método chamado ser efetivamente executado — ou mesmo qualquer instabilidade no HTTP, ou picos de latência, podem tornar o processamento mais lento, devido ao overhead das camadas utilizadas, e com isso deixar o cliente do serviço esperando por muito tempo, o que não é uma boa prática. Como toda a solução tecnológica, seu uso deve ser mediante a necessidade, quando aplicável ao cenário proposto. Se você precisa transferir arquivos grandes para um processamento, faça-o via FTP, e utilize um WebService apenas para solicitar a requisição de processamento, informando o arquivo a ser utilizado. Lembrando que esta é uma prerrogativa que o desenvolvedor da parte “Server” do Web Service deve ser responsável.

Web Services no AdvPL

Desde 2002, o Protheus permite a criação de Web Services Server no repositório, bastando para isso escrever uma classe do tipo WSSERVER, e um FrameWork da LIB do ERP Microsiga provê a camada WEB de publicação ( WSDL ) e consumo. E, para a geração de uma classe proxy de um WebService, o IDE e o TDS possuem uma ferramenta de geração de código, que gera a classe client AdvPL ( WSCLIENT ), também utilizando um FrameWork da LIB do ERP Microsiga, para consumir o serviço.

A implementação dos Web Services em AdvPL (client-side) não consegue atender a especificação inteira de todas as versões de SOAP, mas atende a maioria das partes mais utilizadas das especificações. Existem algumas dificuldades com o uso de WebServices onde o WSDL “mescla” namespaces distintos nos serviços, e às vezes alguma propriedade específica da especificação é utilizada, e exige um pacote diferenciado. Uma vez que a classe proxy seja gerada em AdvPL, é possível customizá-la para atender estas necessidades, mas o desenvolvedor deve lembrar-se de que: Se ele precisar re-gerar a classe proxy, as alterações feitas por ele no código serão perdidas, portanto quando isso for mesmo necessário, cabe ao desenvolvedor manter um histórico do que ele precisou mexer na classe client, para ele refazer as mesmas alterações caso a classe client venha a ser re-gerada.

No TDN, existe uma árvore de tópicos sobre Web Services, dispinível a partir do Link http://tdn.totvs.com/display/tec/Web+Services+–+23597, onde inclusive é abordado um outro tipo de Web Service, chamado REST, que não usa XML para a troca de dados entre a aplicação cliente e servidora. Este post não substitui a documentação da TDN, apenas procura colocar alguns detalhes a mais, em uma linguagem um pouco mais informal, sobre a tecnologia envolvida.

Nova classe Client no AdvPL

Nas builds do TOTVS|Application Server, a partir da 7.00.121227P, foi publicada uma nova classe client de Web Services na linguagem AdvPL, chamada tWSDLManager. Ela é uma classe proxy dinâmica, onde você fornece para ela o WSDL do serviço, ela identifica na hora todas as classes e métodos disponíveis para serem consumidas, e provê métodos para que você consuma o serviço diretamente pela classe, ou utilize a classe como “core” de WebSErvices, para montar o pacote com a requisição, e o desenvolvedor poder utilizar outro meio de transporte além do HTTP para a transmissão dos dados.

Ela não é tão prática quanto a implementação anterior, onde uma classe proxy em AdvPL era gerada e compilada no repositório de objetos do ERP, porém ela foi construída com um núcleo mais robusto, e compatível com elementos não suportados pela classe proxy WSCLIENT. Então, este tópico vamos ver como usar esta classe para, por exemplo, consumir um Web Service publico na Internet.

Usando um Web Service na Internet

Vamos usar um serviço de conversão de moedas, disponível no endereço http://www.webservicex.net/CurrencyConvertor.asmx. Ao acessar esta página,
vemos que não se trata de um Web Site tradicional, mas uma página de publicação com detalhes do serviço. O provedor deste serviço utilizou o ASP .NET para fazê-lo. O que nos interessa para consumir o serviço é o WSDL, disponivel no link da página onde é mostrado em destaque “Service Description”. Clique neste link, e o WSDL será mostrado no seu Browser. Agora, pegue a URL mostrada no Browse ( http://www.webservicex.net/CurrencyConvertor.asmx?WSDL ) e copie para a área de transferência do Windows ( o famoso Control+C ).

Gerando a classe Client em AdvPL

Pelo método tradicional, usando o TDS, vamos gerar a classe client em AdvPL para consumir este serviço. Antes de mais nada, após abrir o TDS, entre na perspectiva de “Administrador TOTVS”, e na aba “Servidor”, configure ou selecione um servidor do TOTVS Application Server previamente configurado e conecte nele. Sem a conexão com um Application Server, a geração de código AdvPL para WebServices não funciona.

Utilizando o TDS 11.3, na perspectiva “Desenvolvedor Totvs”, localize a janela “Explorador de Projetos”, normalmente do lado esquerdo do vídeo, escolha ou crie uma pasta no projeto para acomodar o fonte gerado, e clicando com o botão direito sobre a pasta, escolha a opção Novo -> Outras, e na tela apresentada, escolha Advpl -> Novo fonte WSDL AdvPL.

Clique em “avançar”, e será mostrado uma tela perguntando os dados pertinentes do serviço. Na primeira informação (local), é mostrada a pasta do projeto onde o fonte será criado. Pode ser mantida a informação lá existente. Na segunda informação (nome do fonte), devemos informar o nome do fonte AdvPL que será criado com a classe client. No nosso exemplo, informe “ConvMoedas”, e no campo “url”, cole a URL do WSDL do serviço, anteriormente copiada para a área de transferência do Windows na etapa anteriormente descrita. Então, clique no botão “Concluir”.

Se não houver nenhum problema com a conexão com a Internet, o fonte “ConvMoedas.prw” deverá ser criado, e adicionado automaticamente ao Projeto aberto no TDS. PAra compilar o fonte, basta estar com o foco na janela do editor de código deste fonte, e pressionar as teclas Control+F9. Vamos ver o fonte gerado no meu ambiente de testes:

#INCLUDE "PROTHEUS.CH"
#INCLUDE "APWEBSRV.CH"
/* ===============================================================================
WSDL Location http://www.webservicex.net/CurrencyConvertor.asmx?WSDL
Gerado em 03/27/15 21:22:35
Observações Código-Fonte gerado por ADVPL WSDL Client 1.120703
 Alterações neste arquivo podem causar funcionamento incorreto
 e serão perdidas caso o código-fonte seja gerado novamente.
=============================================================================== */
User Function _HKRQIVL ; Return // "dummy" function - Internal Use
/* -------------------------------------------------------------------------------
WSDL Service WSCurrencyConvertor
------------------------------------------------------------------------------- */
WSCLIENT WSCurrencyConvertor
WSMETHOD NEW
 WSMETHOD INIT
 WSMETHOD RESET
 WSMETHOD CLONE
 WSMETHOD ConversionRate
WSDATA _URL AS String
 WSDATA _HEADOUT AS Array of String
 WSDATA _COOKIES AS Array of String
 WSDATA oWSFromCurrency AS CurrencyConvertor_Currency
 WSDATA oWSToCurrency AS CurrencyConvertor_Currency
 WSDATA nConversionRateResult AS double
ENDWSCLIENT
WSMETHOD NEW WSCLIENT WSCurrencyConvertor
::Init()
If !FindFunction("XMLCHILDEX")
 UserException("O Código-Fonte Client atual requer os executáveis do Protheus Build [7.00.131227A-20150220] ou superior. Atualize o Protheus ou gere o Código-Fonte novamente utilizando o Build atual.")
EndIf
If val(right(GetWSCVer(),8)) < 1.040504
 UserException("O Código-Fonte Client atual requer a versão de Lib para WebServices igual ou superior a ADVPL WSDL Client 1.040504. Atualize o repositório ou gere o Código-Fonte novamente utilizando o repositório atual.")
EndIf
Return Self
WSMETHOD INIT WSCLIENT WSCurrencyConvertor
 ::oWSFromCurrency := CurrencyConvertor_CURRENCY():New()
 ::oWSToCurrency := CurrencyConvertor_CURRENCY():New()
Return
WSMETHOD RESET WSCLIENT WSCurrencyConvertor
 ::oWSFromCurrency := NIL 
 ::oWSToCurrency := NIL 
 ::nConversionRateResult := NIL 
 ::Init()
Return
WSMETHOD CLONE WSCLIENT WSCurrencyConvertor
Local oClone := WSCurrencyConvertor():New()
 oClone:_URL := ::_URL 
 oClone:oWSFromCurrency := IIF(::oWSFromCurrency = NIL , NIL ,::oWSFromCurrency:Clone() )
 oClone:oWSToCurrency := IIF(::oWSToCurrency = NIL , NIL ,::oWSToCurrency:Clone() )
 oClone:nConversionRateResult := ::nConversionRateResult
Return oClone
// WSDL Method ConversionRate of Service WSCurrencyConvertor
WSMETHOD ConversionRate WSSEND oWSFromCurrency,oWSToCurrency WSRECEIVE nConversionRateResult WSCLIENT WSCurrencyConvertor
Local cSoap := "" , oXmlRet
BEGIN WSMETHOD
cSoap += ''
cSoap += WSSoapValue("FromCurrency", ::oWSFromCurrency, oWSFromCurrency , "Currency", .T. , .F., 0 , NIL, .F.) 
cSoap += WSSoapValue("ToCurrency", ::oWSToCurrency, oWSToCurrency , "Currency", .T. , .F., 0 , NIL, .F.) 
cSoap += ""
oXmlRet := SvcSoapCall( Self,cSoap,; 
 "http://www.webserviceX.NET/ConversionRate",; 
 "DOCUMENT","http://www.webserviceX.NET/",,,; 
 "http://www.webservicex.net/CurrencyConvertor.asmx")
::Init()
::nConversionRateResult := WSAdvValue( oXmlRet,"_CONVERSIONRATERESPONSE:_CONVERSIONRATERESULT:TEXT","double",NIL,NIL,NIL,NIL,NIL,NIL)
END WSMETHOD
oXmlRet := NIL
Return .T.
// WSDL Data Enumeration Currency
WSSTRUCT CurrencyConvertor_Currency
 WSDATA Value AS string
 WSDATA cValueType AS string
 WSDATA aValueList AS Array Of string
 WSMETHOD NEW
 WSMETHOD CLONE
 WSMETHOD SOAPSEND
 WSMETHOD SOAPRECV
ENDWSSTRUCT
WSMETHOD NEW WSCLIENT CurrencyConvertor_Currency
 ::Value := NIL
 ::cValueType := "string"
 ::aValueList := {}
 aadd(::aValueList , "AFA" )
 aadd(::aValueList , "ALL" )
 aadd(::aValueList , "DZD" )
 aadd(::aValueList , "ARS" )
 aadd(::aValueList , "AWG" )
 aadd(::aValueList , "AUD" )
 aadd(::aValueList , "BSD" )
 aadd(::aValueList , "BHD" )
 aadd(::aValueList , "BDT" )
 aadd(::aValueList , "BBD" )
 aadd(::aValueList , "BZD" )
 aadd(::aValueList , "BMD" )
 aadd(::aValueList , "BTN" )
 aadd(::aValueList , "BOB" )
 aadd(::aValueList , "BWP" )
 aadd(::aValueList , "BRL" )
 aadd(::aValueList , "GBP" )
 aadd(::aValueList , "BND" )
 aadd(::aValueList , "BIF" )
 aadd(::aValueList , "XOF" )
 aadd(::aValueList , "XAF" )
 aadd(::aValueList , "KHR" )
 aadd(::aValueList , "CAD" )
 aadd(::aValueList , "CVE" )
 aadd(::aValueList , "KYD" )
 aadd(::aValueList , "CLP" )
 aadd(::aValueList , "CNY" )
 aadd(::aValueList , "COP" )
 aadd(::aValueList , "KMF" )
 aadd(::aValueList , "CRC" )
 aadd(::aValueList , "HRK" )
 aadd(::aValueList , "CUP" )
 aadd(::aValueList , "CYP" )
 aadd(::aValueList , "CZK" )
 aadd(::aValueList , "DKK" )
 aadd(::aValueList , "DJF" )
 aadd(::aValueList , "DOP" )
 aadd(::aValueList , "XCD" )
 aadd(::aValueList , "EGP" )
 aadd(::aValueList , "SVC" )
 aadd(::aValueList , "EEK" )
 aadd(::aValueList , "ETB" )
 aadd(::aValueList , "EUR" )
 aadd(::aValueList , "FKP" )
 aadd(::aValueList , "GMD" )
 aadd(::aValueList , "GHC" )
 aadd(::aValueList , "GIP" )
 aadd(::aValueList , "XAU" )
 aadd(::aValueList , "GTQ" )
 aadd(::aValueList , "GNF" )
 aadd(::aValueList , "GYD" )
 aadd(::aValueList , "HTG" )
 aadd(::aValueList , "HNL" )
 aadd(::aValueList , "HKD" )
 aadd(::aValueList , "HUF" )
 aadd(::aValueList , "ISK" )
 aadd(::aValueList , "INR" )
 aadd(::aValueList , "IDR" )
 aadd(::aValueList , "IQD" )
 aadd(::aValueList , "ILS" )
 aadd(::aValueList , "JMD" )
 aadd(::aValueList , "JPY" )
 aadd(::aValueList , "JOD" )
 aadd(::aValueList , "KZT" )
 aadd(::aValueList , "KES" )
 aadd(::aValueList , "KRW" )
 aadd(::aValueList , "KWD" )
 aadd(::aValueList , "LAK" )
 aadd(::aValueList , "LVL" )
 aadd(::aValueList , "LBP" )
 aadd(::aValueList , "LSL" )
 aadd(::aValueList , "LRD" )
 aadd(::aValueList , "LYD" )
 aadd(::aValueList , "LTL" )
 aadd(::aValueList , "MOP" )
 aadd(::aValueList , "MKD" )
 aadd(::aValueList , "MGF" )
 aadd(::aValueList , "MWK" )
 aadd(::aValueList , "MYR" )
 aadd(::aValueList , "MVR" )
 aadd(::aValueList , "MTL" )
 aadd(::aValueList , "MRO" )
 aadd(::aValueList , "MUR" )
 aadd(::aValueList , "MXN" )
 aadd(::aValueList , "MDL" )
 aadd(::aValueList , "MNT" )
 aadd(::aValueList , "MAD" )
 aadd(::aValueList , "MZM" )
 aadd(::aValueList , "MMK" )
 aadd(::aValueList , "NAD" )
 aadd(::aValueList , "NPR" )
 aadd(::aValueList , "ANG" )
 aadd(::aValueList , "NZD" )
 aadd(::aValueList , "NIO" )
 aadd(::aValueList , "NGN" )
 aadd(::aValueList , "KPW" )
 aadd(::aValueList , "NOK" )
 aadd(::aValueList , "OMR" )
 aadd(::aValueList , "XPF" )
 aadd(::aValueList , "PKR" )
 aadd(::aValueList , "XPD" )
 aadd(::aValueList , "PAB" )
 aadd(::aValueList , "PGK" )
 aadd(::aValueList , "PYG" )
 aadd(::aValueList , "PEN" )
 aadd(::aValueList , "PHP" )
 aadd(::aValueList , "XPT" )
 aadd(::aValueList , "PLN" )
 aadd(::aValueList , "QAR" )
 aadd(::aValueList , "ROL" )
 aadd(::aValueList , "RUB" )
 aadd(::aValueList , "WST" )
 aadd(::aValueList , "STD" )
 aadd(::aValueList , "SAR" )
 aadd(::aValueList , "SCR" )
 aadd(::aValueList , "SLL" )
 aadd(::aValueList , "XAG" )
 aadd(::aValueList , "SGD" )
 aadd(::aValueList , "SKK" )
 aadd(::aValueList , "SIT" )
 aadd(::aValueList , "SBD" )
 aadd(::aValueList , "SOS" )
 aadd(::aValueList , "ZAR" )
 aadd(::aValueList , "LKR" )
 aadd(::aValueList , "SHP" )
 aadd(::aValueList , "SDD" )
 aadd(::aValueList , "SRG" )
 aadd(::aValueList , "SZL" )
 aadd(::aValueList , "SEK" )
 aadd(::aValueList , "CHF" )
 aadd(::aValueList , "SYP" )
 aadd(::aValueList , "TWD" )
 aadd(::aValueList , "TZS" )
 aadd(::aValueList , "THB" )
 aadd(::aValueList , "TOP" )
 aadd(::aValueList , "TTD" )
 aadd(::aValueList , "TND" )
 aadd(::aValueList , "TRL" )
 aadd(::aValueList , "USD" )
 aadd(::aValueList , "AED" )
 aadd(::aValueList , "UGX" )
 aadd(::aValueList , "UAH" )
 aadd(::aValueList , "UYU" )
 aadd(::aValueList , "VUV" )
 aadd(::aValueList , "VEB" )
 aadd(::aValueList , "VND" )
 aadd(::aValueList , "YER" )
 aadd(::aValueList , "YUM" )
 aadd(::aValueList , "ZMK" )
 aadd(::aValueList , "ZWD" )
 aadd(::aValueList , "TRY" )
Return Self
WSMETHOD SOAPSEND WSCLIENT CurrencyConvertor_Currency
 Local cSoap := "" 
 cSoap += WSSoapValue("Value", ::Value, NIL , "string", .F. , .F., 3 , NIL, .F.) 
Return cSoap
WSMETHOD SOAPRECV WSSEND oResponse WSCLIENT CurrencyConvertor_Currency
 ::Value := NIL
 If oResponse = NIL ; Return ; Endif 
 ::Value := oResponse:TEXT
Return
WSMETHOD CLONE WSCLIENT CurrencyConvertor_Currency
Local oClone := CurrencyConvertor_Currency():New()
 oClone:Value := ::Value
Return oClone

Agora, para consumir este serviço, precisamos dar uma olhada com calma na classe proxy gerada. Primeiro, vejamos os métodos disponíveis:

WSMETHOD NEW
 WSMETHOD INIT
 WSMETHOD RESET
 WSMETHOD CLONE
 WSMETHOD ConversionRate

Os quatro primeiros métodos fazem parte da manutenção da instância da classe client. Por hora o que nos interessa é o construtor (NEW), e o método ConversionRate, que é o único método disponibilizado por este serviço. Vamos procurar pela declaração deste método.

WSMETHOD ConversionRate WSSEND oWSFromCurrency,oWSToCurrency WSRECEIVE nConversionRateResult WSCLIENT WSCurrencyConvertor

Ao avaliarmos a implementação do método, vemos que ele têm dois parâmetros, do tipo Objeto, e um resultado numérico. Vamos procurar no fonte pela definição de cada um destes parâmetros (oWSFromCurrency e oWSToCurrency).

WSDATA oWSFromCurrency AS CurrencyConvertor_Currency
WSDATA oWSToCurrency AS CurrencyConvertor_Currency

Cada um dos parâmetros do WebService é declarado como uma propriedade da classe Proxy. Neste caso, ambos são objetos do mesmo tipo ( CurrencyConvertor_Currency ). Vamos ver definição deste objeto:

// WSDL Data Enumeration Currency
WSSTRUCT CurrencyConvertor_Currency
 WSDATA Value AS string
 WSDATA cValueType AS string
 WSDATA aValueList AS Array Of string
 WSMETHOD NEW
 WSMETHOD CLONE
 WSMETHOD SOAPSEND
 WSMETHOD SOAPRECV
ENDWSSTRUCT

Então, este objeto é um “Enumeration”, com base em uma String. Isto permite o código Client do Web Services verificar se o conteúdo informado é válido antes mesmo da requisição ser realizada.

Com estas informações, vamos montar o fonte Client em AdvPL que vai consumir esta classe.

#include "protheus.ch"
/* -------------------------------------------------------------------
Função U_MOEDAS
Autor Júlio Wittwer
Data 28/03/2015
Descrição Fonte de exemplo consumingo um Web Service publico
 de fator de conversão de moedas, utilizando a 
 geração de classe Client de Web Services do AdvPL
Url http://www.webservicex.net/CurrencyConvertor.asmx?WSDL
------------------------------------------------------------------- */
User Function Moedas()
Local oWS
// Cria a instância da classe client
oWs := WSCurrencyConvertor():New()
// Alimenta as propriedades de envio 
oWS:oWSFromCurrency:Value := 'BRL' // Real ( Brasil )
oWS:oWSToCurrency:Value := 'USD' // United States Dollar
// Habilita informações de debug no log de console
WSDLDbgLevel(3)
// Chama o método do Web Service
If oWs:ConversionRate()
 // Retorno .T. , solicitação enviada e recebida com sucesso
 MsgStop("Fator de conversão: "+cValToChar(oWS:nConversionRateResult),"Requisição Ok")
 MsgStop("Por exemplo, 100 reais compram "+cValToChar(100 * oWS:nConversionRateResult )+" Dólares Americanos.")
Else
 // Retorno .F., recupera e mostra string com detalhes do erro 
 MsgStop(GetWSCError(),"Erro de Processamento")
Endif
Return
Ao executar este fonte, o retorno esperado é uma sequência de duas janelas de informações, contendo o fator de conversão, e um exemplo de uso deste fator convertendo 100 reais para dólares americanos.
Agora, vamos ver como este exemplo seria escrito usando a classe tWSDLManager(), da forma mais minimalista possível.
#include "protheus.ch"
/* -------------------------------------------------------------------
Função U_MOEDAS2
Autor Júlio Wittwer
Data 28/03/2015
Descrição Fonte de exemplo consumingo um Web Service publico
 de fator de conversão de moedas, utilizando a 
 classe tWSDLManager()
Url http://www.webservicex.net/CurrencyConvertor.asmx?WSDL
------------------------------------------------------------------- */
User Function Moedas2()
Local oWSDL
Local lOk, cResp, aElem, nPos
oWSDL := tWSDLManager():New()
// Seta o modo de trabalho da classe para "verbose"
oWSDL:lVerbose := .T.
// Primeiro faz o parser do WSDL a partir da URL
lOk := oWsdl:ParseURL( "http://www.webservicex.net/CurrencyConvertor.asmx?WSDL" )
if !lOk 
 MsgStop( oWsdl:cError , "ParseURL() ERROR")
 Return
endif
// Seta a operação a ser utilizada
lOk := oWsdl:SetOperation( "ConversionRate" )
if !lOk
 MsgStop( oWsdl:cError , "SetOperation(ConversionRate) ERROR")
 Return
endif
// Setar um valor para conversão
lOk := oWsdl:SetFirst('FromCurrency','BRL')
if !lOk
 MsgStop( oWsdl:cError , "SetFirst(FromCurrency) ERROR")
 Return
endif
lOk := oWsdl:SetFirst('ToCurrency','USD')
if !lOk
 MsgStop( oWsdl:cError , "SetFirst (ToCurrency) ERROR")
 Return
endif/
// Faz a requisição ao WebService 
lOk := oWsdl:SendSoapMsg()
if !lOk
 MsgStop( oWsdl:cError , "SendSoapMsg() ERROR")
 Return
endif
// Recupera os elementos de retorno, já parseados
cResp := oWsdl:GetParsedResponse()
// Monta um array com a resposta parseada, considerando
// as quebras de linha ( LF == Chr(10) ) 
aElem := StrTokArr(cResp,chr(10))
nPos := ascan(aElem,{|x| left(x,21) == 'ConversionRateResult:'}) 
If nPos > 0 
 nFator := val( substr(aElem[nPos],22) )
 MsgStop("Fator de conversão: "+cValToChar(nFator),"Requisição Ok")
 MsgStop("Por exemplo, 100 reais compram "+cValToChar(100 * nFator )+" Dólares Americanos.")
Else
 MsgStop("Resposta não encontrada ou inválida.")
Endif
Return

Para utilizar este exemplo, é necessário criar um arquivo para conter o Fonte Client da classe AdvPL gerada, e outro para compilar o fonte que consome esta classe client, e para executar, basta chamar os fontes U_Moedas e U_Moedas2 a partir do SmartClient.

A classe tWSDLManager, embora seja superior em relação ao SOAP, ainda precisa de algumas informações adicionais para se tornar mais prática para uso. Para montar o exemplo acima, foi necessário codificar um fonte um pouco maior, chamando os métodos adicionais da classe para obter os nomes dos serviços publicados, os nomes dos elementos de parâmetro e retorno do serviço desejado, e verificar o formato de retorno parseado para extrair o valor retornado pelo Web Service.

Nos próximos tópicos sobre Web Services, vamos abordar entre outras coisas a utilização de serviços que requerem autenticação e/ou conexão segura (SSL) utilizando chaves criptográficas, e como tratar de forma adequada as falhas que podem ocorrer no lado de uma aplicação client de Web Services.

Conclusão

Este post foi só pra abrir o apetite, têm muito mais a ser explorado neste universo. Mais uma vez, agradeço a todos pela audiência do Blog, e espero vê-los por aqui mais vezes. Até o próximo post, pessoal 😉