segunda-feira, 4 de maio de 2015

Como consumir webService SOAP no Android - Parte 2 (complexTypes)

Em nosso primeiro post sobre webServices SOAP (Como consumir webService SOAP no Android), aprendemos a utilizar serviços remotos disponibilizados em SOAP utilizando a biblioteca KSOAP2.

Entretanto, o exemplo de consumo que fizemos na ocasião objetivava apenas mostrar o funcionamento básico, pois o serviço que experimentamos (de conversão de temperaturas de graus Celsius para Farenheit) retorna apenas um primitivo (string). Na vida real, quando estivermos consumindo webServices SOAP de sistemas já em produção, será muito mais comum nos depararmos com tipos complexos (os "complexTypes") definidos nos arquivos WSDL.

"ComplexType" não é nada além de um outro nome para "estrutura de dados", ou "descrição dos atributos". É uma maneira de descrever os campos de um registro (ou os atributos de um objeto da classe) para permitir a integração de dois ou mais sistemas.

Utilizaremos neste tutorial um serviço de consulta e inclusão de funcionários, que criei especialmente para o exemplo. O serviço pode ser consumido pelo seguinte link:




Embora seja possível abrir o link no SoapUI para visualização de suas características e simular o consumo, isto não será imprescindível, uma vez que já conhecemos os métodos disponíveis.

Este serviço possui dois métodos. O primeiro chamado "buscarFuncionario", que tem como único parâmetro "matricula" (numérico). Fiz uma pequena base de dados com 10 funcionários fictícios (matrículas de 1 a 10) e criei um "tipo complexo" com a seguinte estrutura:


funcionarioDTO
   matricula : Double
   nome: string
   sobrenome: string
   cargo: string
   salario: Double



Em nosso WSDL, a descrição aparecerá assim:


<complexType name="FuncionarioDTO">
   <sequence>
      <element name="matricula" nillable="true" type="xsd:double"/>
      <element name="nome" nillable="true" type="xsd:string"/>
      <element name="sobrenome" nillable="true" type="xsd:string"/>
      <element name="cargo" nillable="true" type="xsd:string"/>
      <element name="salario" nillable="true" type="xsd:double"/>
   </sequence>
</complexType>


O segundo método se chama "gravarFuncionario". O parâmetro de entrada esperado por ele é justamente o "complexType" FuncionarioDTO. Para criarmos um funcionário novo na base de dados remota, devemos instanciar um objeto da classe FuncionarioDTO, preencher seus atributos e então passá-lo como argumento para o método do webService SOAP.

De posse dessas informações, vamos então, partir para a implementação do consumo.

Precisaremos criar a classe FuncionarioDTO implementando KvmSerializeable, o que pode ser visto no código-fonte abaixo:


package com.example.consumidorsoapcomplextype;

import java.util.Hashtable;
import org.ksoap2.serialization.KvmSerializable;
import org.ksoap2.serialization.PropertyInfo;

public class FuncionarioDTO implements KvmSerializable
{
  
   private Double matricula;
   private String nome="";
   private String sobrenome="";
   private String cargo="";
   private Double salario;
      
   public Double getMatricula()
   {
      return matricula;
   }

   public void setMatricula(Double matricula)
   {
      this.matricula=matricula;
   }

   public String getNome()
   {
      return nome;
   }

   public void setNome(String nome)
   {
      this.nome=nome;
   }

   public String getSobrenome()
   {
      return sobrenome;
   }

   public void setSobrenome(String sobrenome)
   {
      this.sobrenome=sobrenome;
   }

   public String getCargo()
   {
      return cargo;
   }

   public void setCargo(String cargo)
   {
      this.cargo=cargo;
   }

   public Double getSalario()
   {
      return salario;
   }

   public void setSalario(Double salario)
   {
      this.salario=salario;
   }

   @Override
   public Object getProperty(int arg0)
   {
      switch (arg0)
      {
         case 0:
            return matricula;
         case 1:
            return nome;
         case 2:
            return sobrenome;
         case 3:
            return cargo;
         case 4:
            return salario;
      }
      return null;
   }

   @Override
   public int getPropertyCount()
   {
      return 5;
   }

   @Override
   public void getPropertyInfo(int arg0, Hashtable arg1, PropertyInfo arg2)
   {
      switch (arg0)
      {
         case 0:
            arg2.type=PropertyInfo.LONG_CLASS;
            arg2.name="matricula";
            break;
         case 1:
            arg2.type=PropertyInfo.STRING_CLASS;
            arg2.name="nome";
            break;
         case 2:
            arg2.type=PropertyInfo.STRING_CLASS;
            arg2.name="sobrenome";
            break;
         case 3:
            arg2.type=PropertyInfo.STRING_CLASS;
            arg2.name="cargo";
            break;
         case 4:
            arg2.type=PropertyInfo.LONG_CLASS;
            arg2.name="salario";
            break;

         default:
            break;
      }
   }

   @Override
   public void setProperty(int arg0, Object arg1)
   {
      switch (arg0)
      {
         case 0:
            matricula=Double.parseDouble(arg1.toString());
            break;
         case 1:
            nome=arg1.toString();
            break;
         case 2:
            sobrenome=arg1.toString();
            break;
         case 3:
            cargo=arg1.toString();
            break;
         case 4:
            salario=Double.parseDouble(arg1.toString());
            break;

         default:
            break;
      }

   }


}


Partiremos então para os métodos. Primeiro criaremos o método "buscarFuncionario", descrito abaixo:

// Método que executa operação de leitura pelo webService.
public FuncionarioDTO buscarFuncionario(double matricula)
{

   // Define DTO que receberá o resultado.
   FuncionarioDTO funcionarioDTO=new FuncionarioDTO();

   // Define o namespace, a URL do endpoint, e a ação do webService.
   String NAMESPACE="http://dadosabertos.rio.rj.gov.br/apiColaboracao/apresentacao/soap/WSFuncionario.cfc";
   String URL="http://dadosabertos.rio.rj.gov.br/apiColaboracao/apresentacao/soap/WSFuncionario.cfc?wsdl";
   String END_POINT="http://dadosabertos.rio.rj.gov.br/apiColaboracao/apresentacao/soap/WSFuncionario.cfc";
   String METHOD_NAME="buscarFuncionario";
   String SOAP_ACTION=END_POINT + METHOD_NAME;

   // Define quantos milissegundos o KSOAP2 aguardará antes de considerar que
   // a requisição falhou.
   int timeout=20000;

   // Declarações das variáveis necessárias para criar a requisição e
   // transportá-la.
   HttpTransportSE ht;
   SoapObject request;

   // Cria a requisição SOAP, para consumir o webService.
   request=new SoapObject(NAMESPACE, METHOD_NAME);

   // Cria o envelope.
   SoapSerializationEnvelope envelope=new SoapSerializationEnvelope(SoapEnvelope.VER11);
   envelope.dotNet=false;

   // Acrescenta o parâmetro ao argumento do webService.
   request.addProperty("matricula", String.valueOf(matricula));

   // Insere a requisição montada no envelope.
   envelope.setOutputSoapObject(request);
   
   // Define o objeto que efetuará o transporte. Aqui também é definido o
   // timeout.
   ht=new HttpTransportSE(Proxy.NO_PROXY, URL, timeout);
   
   try
   {
      // Efetivamente chama o consumo do webService.
      ht.call(SOAP_ACTION, envelope);

      // Obtém a resposta e realiza o mapeamento com o objeto funcionarioDTO.
      SoapObject rs = (SoapObject) envelope.getResponse();
      funcionarioDTO.setMatricula(Double.parseDouble(rs.getPropertySafelyAsString("matricula")));
      funcionarioDTO.setNome(rs.getPropertySafelyAsString("nome"));
      funcionarioDTO.setSobrenome(rs.getPropertySafelyAsString("sobrenome"));
      funcionarioDTO.setCargo(rs.getPropertySafelyAsString("cargo"));
      funcionarioDTO.setSalario(Double.parseDouble(rs.getPropertySafelyAsString("salario")));
      
   }
   catch (Exception e)
   {
      // Caso tenha ocorrido algum problema, indica a mensagem de erro no
      // LogCat.
      Log.i("consumidorSoap", "(MainActivity, método buscarFuncionario) erro: " + e.toString());
      funcionarioDTO = null;
   }

   // Limpa os objetos instanciados, para liberar memória.
   request=null;
   envelope=null;
   ht=null;

   // Retorna o resultado.
   return funcionarioDTO;
}

Nosso método chama o webService remoto passando uma matrícula de funcionário e recebe de volta um "complexType" do tipo FuncionarioDTO, preenchido.

A próxima tarefa é implementar o método de gravação do funcionário. Segue abaixo o código-fonte:


// Método que executa operação de gravação através do webService, passando um objeto da classe "FuncionarioDTO" (complexType).
public String gravarFuncionario(Double matricula, String nome, String sobrenome, String cargo, Double salario)
{
   // Declaração da variável que armazenará o resultado do consumo.
   String resultado="";      
   
   // Define o namespace, a URL do endpoint, e a ação do webService.
   String NAMESPACE="http://dadosabertos.rio.rj.gov.br/apiColaboracao/apresentacao/soap/WSFuncionario.cfc";
   String URL="http://dadosabertos.rio.rj.gov.br/apiColaboracao/apresentacao/soap/WSFuncionario.cfc?wsdl";
   String END_POINT="http://dadosabertos.rio.rj.gov.br/apiColaboracao/apresentacao/soap/WSFuncionario.cfc";
   String METHOD_NAME="gravarFuncionario";
   String SOAP_ACTION=END_POINT + METHOD_NAME;

   // Define quantos milissegundos o KSOAP2 aguardará antes de considerar que
   // a requisição falhou.
   int timeout=20000;

   // Declarações das variáveis necessárias para criar a requisição e
   // transportá-la.
   HttpTransportSE ht;
   SoapObject request;
   PropertyInfo propInfo;

   // Cria a requisição SOAP, para consumir o webService.
   request=new SoapObject(NAMESPACE, METHOD_NAME);

   // Cria o envelope.
   SoapSerializationEnvelope envelope=new SoapSerializationEnvelope(SoapEnvelope.VER11);
   envelope.dotNet=false;

   // Instancia um objeto da classe FuncionarioDTO e o preenche com os parâmetros do método.
   FuncionarioDTO funcionarioDTO = new FuncionarioDTO();
   funcionarioDTO.setMatricula(matricula);
   funcionarioDTO.setNome(nome);
   funcionarioDTO.setSobrenome(sobrenome);
   funcionarioDTO.setCargo(cargo);
   funcionarioDTO.setSalario(salario);
   
   // Cria o argumento exigido pelo método do webService (um complexType FuncionarioDTO) e o acrescenta à requisição.
   // Isto é feito com a classe PropertyInfo.
   propInfo = new PropertyInfo(); 
   propInfo.name = "FuncionarioDTO";
   propInfo.type=funcionarioDTO.getClass();

   // Acrescenta à requisição o argumento necessário para o consumo do método de gravação do webService,
   // que espera um "complexType" FuncionarioDTO. Aqui associa-se o objeto funcionarioDTO (preenchido),
   // à descrição que fizemos via PropertyInfo.
   request.addPropertyIfValue(propInfo, funcionarioDTO);

   // O "complexType" FuncionarioDTO no wsdl aparece dentro de um NAMESPACE.
   // Devemos fazer um mapeamento para colocar nosso objeto no local correto (FuncionarioDTO do targetNamespace
   // dto.dominio.apicolaboracao).
   envelope.addMapping("http://dto.dominio.apicolaboracao", "FuncionarioDTO", funcionarioDTO.getClass());
   
   // Para usar classes que tenham elementos "Double" ou "Date" com o KSOAP2, é necessário
   // regularizá-las. Isso é feito através do uso das classes MarshalDouble ou MarshalDate.
   // As classes são de autoria de Vladimir, do site http://seesharpgears.blogspot.com.br/,
   // ótima fonte de conhecimento sobre KSOAP2.
   MarshalDouble md = new MarshalDouble();
   md.register(envelope);
   
   // Insere a requisição montada no envelope.
   envelope.setOutputSoapObject(request);

   // Define o objeto que efetuará o transporte. Aqui também é definido o
   // timeout.
   ht=new HttpTransportSE(Proxy.NO_PROXY, URL, timeout);
   
   // A opção de debug permite a impressão das mensagens em caso de erro.
   // Aqui estamos utilizando para ver o request e o response, caso haja exceção.
   ht.debug = true;
   
   try
   {
      // Efetivamente chama o consumo do webService.
      ht.call(SOAP_ACTION, envelope);

      // Obtém a resposta da gravação.
      resultado = "" + envelope.getResponse();
      
   }
   catch (Exception e)
   {
      // Caso tenha ocorrido algum problema, indica a mensagem de erro no
      // LogCat.
      Log.i("consumidorSoap", "(MainActivity, método gravarFuncionario) erro: " + e.toString());
      
      // As linhas abaixo permitem ver a requisição que foi montada para o consumo do serviço
      // e também a resposta do servidor. Muito útil para debugging. Exige o "ht.debug = true;"
      Log.i("consumidorSoap", "Request  : " + ht.requestDump); 
      Log.i("consumidorSoap", "Response : " + ht.responseDump);
      resultado = "Ocorreu um erro. Gravação não realizada.";
   }

   // Limpa os objetos instanciados, para liberar memória.
   request=null;
   envelope=null;
   ht=null;

   // Retorna o resultado.
   return resultado;
}

Aqui, cabem algumas observações importantes: Tivemos que instanciar um objeto da classe FuncionarioDTO, preencher seus atributos e informar ao KSOAP2 que o tipo que seguiria como argumento seria FuncionarioDTO:

// Instancia um objeto da classe FuncionarioDTO e o preenche com os parâmetros do método.
   FuncionarioDTO funcionarioDTO = new FuncionarioDTO();
   funcionarioDTO.setMatricula(matricula);
   funcionarioDTO.setNome(nome);
   funcionarioDTO.setSobrenome(sobrenome);
   funcionarioDTO.setCargo(cargo);
   funcionarioDTO.setSalario(salario);
   
   // Cria o argumento exigido pelo método do webService (um complexType FuncionarioDTO) e o acrescenta à requisição.
   // Isto é feito com a classe PropertyInfo.
   propInfo = new PropertyInfo(); 
   propInfo.name = "FuncionarioDTO";
   propInfo.type=funcionarioDTO.getClass();

   // Acrescenta à requisição o argumento necessário para o consumo do método de gravação do webService,
   // que espera um "complexType" FuncionarioDTO. Aqui associa-se o objeto funcionarioDTO (preenchido),
   // à descrição que fizemos via PropertyInfo.
   request.addPropertyIfValue(propInfo, funcionarioDTO);


Também tivemos que criar um mapeamento para que fosse posível o webService encontrar nosso objeto dentro da requisição. O mapeamento diz ao webService que estamos colocando o objeto no complexType "FuncionarioDTO" descrito no namespace "http://dto.dominio.apicolaboracao":



// O "complexType" FuncionarioDTO no wsdl aparece dentro de um NAMESPACE.
   // Devemos fazer um mapeamento para colocar nosso objeto no local correto (FuncionarioDTO do targetNamespace
   // dto.dominio.apicolaboracao).
   envelope.addMapping("http://dto.dominio.apicolaboracao", "FuncionarioDTO", funcionarioDTO.getClass());


Foi necessário utilizar a classe MarshalDouble (de autoria de Vladimir), porque nosso complexType possui um campo Double. Infelizmente o KSOAP2 possui alguns problemas com campos Double e Date, portanto, é necessário regularizar esses tipos:

 // Para usar classes que tenham elementos "Double" ou "Date" com o KSOAP2, é necessário
   // regularizá-las. Isso é feito através do uso das classes MarshalDouble ou MarshalDate.
   // As classes são de autoria de Vladimir, do site http://seesharpgears.blogspot.com.br/,
   // ótima fonte de conhecimento sobre KSOAP2.
   MarshalDouble md = new MarshalDouble();
   md.register(envelope);



A última etapa é chamar os métodos, primeiro para ler os dados de um funcionário fornecendo sua matrícula (criei funcionários fictícios com matrículas de 1 a 10) e depois para gravar um novo funcionário, enviando um objeto FuncionarioDTO preenchido (a partir da matrícula número 11), o que é feito da seguinte forma:



// Instancia um objeto da classe FuncionarioDTO, que será utilizado para mapear o resultado do consumo do webService.
FuncionarioDTO funcionario=new FuncionarioDTO();

// Operação de leitura via webService.
// Chama o método que consome o webService SOAP (operação de leitura).
// Experimente mudar o número da matrícula (o parâmetro, com um número de 1 em diante) para ver outros funcionários.
funcionario = buscarFuncionario(1);

// Mostra no LogCat o resultado. Não esqueça de criar uma tag no LogCat denominada "consumidorSoap", para visualizar a saída.
if (funcionario != null)
{
   Log.i("consumidorSoap", "Resultado da operação de leitura pelo webService SOAP:\n");
   Log.i("consumidorSoap", "Matrícula: " + funcionario.getMatricula());
   Log.i("consumidorSoap", "Nome: " + funcionario.getNome());
   Log.i("consumidorSoap", "Sobrenome: " + funcionario.getSobrenome());
   Log.i("consumidorSoap", "Cargo: " + funcionario.getCargo());
   Log.i("consumidorSoap", "Salário: " + funcionario.getSalario() + "\n");
}
else
{
   Log.i("consumidorSoap", "Resultado da operação de leitura pelo webService SOAP:\n" + "Ocorreu um erro durante o consumo do webService\n." );
}

// Operação de gravação via webService.
// Chama o método que grava um novo funcionário através do webService SOAP.
// Experimente alterar os valores e examine os resultados.
Double matricula = 11.0;
String nome="JOSÉ";
String sobrenome="SILVA";
String cargo = "PROGRAMADOR";
Double salario = 3500.00;

String resultado = gravarFuncionario(matricula, nome, sobrenome, cargo, salario);

// Mostra no LogCat o resultado da gravação.
Log.i("consumidorSoap", "\nResultado da operação de gravação pelo webService SOAP:\n");

Log.i("consumidorSoap", resultado);


Os resultados das operações serão mostrados no LogCat do Eclipse (é preciso criar um tag "consumidorSoap").

Para quem desejar baixar o código-fonte completo deste exemplo, disponibilizo neste link para download.

Com isto encerramos o assunto SOAP. Partiremos agora para exemplos de consumo de webServices REST no Android, o que é bem mais tranquilo. 

Até lá!


Nenhum comentário:

Postar um comentário