Android+KSoap – Pt.3: Diga Não à Duplicação

Publicado por Bruno Vinicius
19/10/2011
Categoria:
Tags: , , , , ,

Já abordamos nos artigos anteriores como fazer a transcrição dos SoapObjects em objetos complexos do nosso modelo, bem como uma maneira simples de passar parâmetros baseando-se na infraestrutura criada inicialmente. Essa postagem finalizará a série, apresentando uma nova camada de abstração com a finalidade de evitar a repetição de código, melhorar a manutenabilidade, facilitar a leitura e simplificar os acessos aos serviços remotos.

O Problema

Como já conversamos, e também já vimos no artigo do Ricardo Ushisima, implementar um request a um serviço SOAP através da biblioteca KSoap é um processo simples e mecânico. Frequentemente taxado como “receita de bolo”. É comum em situações assim, acabar sucumbindo a tentação de utilizar a famigerada ferramenta de programação popularmente conhecida por “Control C, Control V”. Não se engane, se inicialmente ele parece inofensivo, em pouco tempo ele vai cobrar seu preço, e você vai amargar um pagamento salgado.

Vamos ilustrar o nosso caso, usando o seguinte código.

public void saveClient(Client client) {
    try {
        SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME);
        PropertyInfo pi = new PropertyInfo();
        pi.name = "client";
        pi.setValue(new Wrapper(client));
        pi.setType(Wrapper.class);
        pi.setNamespace(NAMESPACE_CLIENT);
        request.addProperty(pi);

        SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
        envelope.dotNet = true;
        envelope.setOutputSoapObject(request);
        envelope.addMapping(NAMESPACE_CLIENT, "Client", Wrapper.class);

        HttpTransportSE androidHttpTransport = new HttpTransportSE(URL);
        androidHttpTransport.call(SOAP_ACTION, envelope);
    } catch (Exception e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}

Esse código não tem nada de errado, por si somente. Mas imaginem que, ao invés de uma única chamada ao seu WS você precise chamar 10, 20, 50 métodos. Complicado, não? Isso sem considerar os por menores na hora de lidar com a API do KSoap, que é cheia de armadilhas. Em situações assim a vida pode ficar bem mais difícil que o usual. Mas observem também, que esse código é um exemplo clássico de receita de bolo. E sempre que há código desse tipo, a chance de se encontrar uma maneira de abstrair essas implementações em um contexto mais genérico e seguro, evitando duplicação de código e de problemas, é muito alta.

A Solução

Nota: a solução que apresentada aqui está bastante integrada à infraestrutura criada nos primeiros dois artigos, logo, se você ainda não os leu, melhor dar uma passadinha lá (parte 1, parte 2), ou você pode acabar sem entender o que está se passando.

Vamos fazer o seguinte: primeiro vou mostrar como fica o código que oferece a mesma funcionalidade que o exibido inicialmente, na apresentação do problema, mas que utiliza a solução de abstração desenvolvida para que possamos analisar os benefícios entre as abordagens. Por fim, vou mostrar o code behind, e vamos ver como chegamos a esse resultado. Vejam.

package br.com.zbra.test;

import org.ksoap2.SoapEnvelope;
import org.ksoap2.serialization.PropertyInfo;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.HttpTransportSE;
import br.com.zbra.test.model.Client;

public class SaveClientService {
	private static final String SOAP_ACTION = "http://zbra.com.br/ksoap/SaveClient";
	private static final String METHOD_NAME = "SaveClient";
	private static final String NAMESPACE = "http://zbra.com.br/springtutorial";
	private static final String NAMESPACE_CLIENT = "http://zbra.com.br/springtutorial";
	private static final String URL = "http://192.168.10.103/SpringTutorialService/SaveClientWS.asmx";

	public void saveClient(Client client) {
		try {
			new Caller<List<Client>>()
			.setServerUrl(URL)
			.setNamespace(NAMESPACE)
			.setService(SERVICE_APPLICATION)
			.setMethod(METHOD_NAME)
			.addParameter("client", client)
			.call();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

Observe quão limpo e claro está esse código. Simples e conciso. É impossível olhar pra esse código e não perceber de cara o que ele está fazendo (dado o contexto do problema, claro). Todo código que poderia estar repetido, agora está encapsulado. Isso significa que qualquer bug ou situação inesperada que encontrarmos no decorrer do desenvolvimento vai ser corrigido em um único ponto. Manutenções evolutivas também se beneficiam da nova implementação. Só há vantagens a enumerar. [too much self congratulatory bullshit maybe?]

Code Behind

Toda a mágica é feita pela classe Caller. Utilizando as anotações já feitas nas classes básicas, ela toma algumas decisões e faz a chamada através do KSoap. Uma outra vantagem bem acentuada desse mecanismo é esconder as nuances da utilização da biblioteca, que é cheia de detalhes e minúcias. Vamos ver o que a classe Caller faz por baixo dos panos.

package br.com.zbra.test.soap;

import java.io.IOException;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import org.ksoap2.SoapEnvelope;
import org.ksoap2.SoapFault;
import org.ksoap2.serialization.PropertyInfo;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapPrimitive;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.HttpTransportSE;
import org.xmlpull.v1.XmlPullParserException;
import br.com.zbra.test.soap.annotations.SOAPObject;
import br.com.zbra.test.soap.annotations.SOAPProperty;
import br.com.zbra.test.soap.serializable.Wrapper;

public class Caller<T> {
    // Static Constants ---------------------------------------------------------------------------
    private static final char URL_SEPARATOR = '/';
    private static final char INTERFACE_PREFIX = 'I';

    // Attributes ---------------------------------------------------------------------------------
    private URL serverUrl;
    private String namespace;
    private String method;
    private String service;
    private String serviceInterface;
    private LinkedList<Mapping> mappings;
    private LinkedHashMap<String, Object> params;
    private IMethodResponseParser<T> responseParser;

    // Public Methods -----------------------------------------------------------------------------
    /**
     * Default constructor for this class.
     */
    public Caller() {
        this.params = new LinkedHashMap<String, Object>();
        this.mappings = new LinkedList<Mapping>();
    }    

    /**
     * Sets the server URL for accessing the remote method.
     *
     * @param serverUrl
     *            the server URL
     */
    public Caller<T> setServerUrl(URL serverUrl) {
        this.serverUrl = serverUrl;
        return this;
    }    

    /**
     * Setup which method will be used when invoking the web service.
     *
     * @param methodName
     *            the method name
     */
    public Caller<T> setMethod(String methodName) {
        this.method = methodName;
        return this;
    }    

    /**
     * Set service's name. This specifies which service the method will be
     * invoked on. If not previously specified, also sets the service interface
     * name by adding the "I" prefix to the service name.
     *
     * @param serviceName
     *            the service name
     */
    public Caller<T> setService(String serviceName) {
        this.service = serviceName;
        this.serviceInterface = INTERFACE_PREFIX + serviceName;
        return this;
    }

    /**
     * Set service's interface name. This specifies which service the method will be
     * invoked on. If your service has a interface following the IServiceName pattern,
     * calling this method is optional.
     *
     * @param serviceInterface
     *            the service interface name.
     */
    public Caller<T> setServiceInterface(String serviceInterface) {
        this.serviceInterface = serviceInterface;
        return this;
    }

    /**
     * Sets the service namespace. If not explicitly specified, the default
     * value is used.
     *
     * @param namespace
     *            the service namespace
     */
    public Caller<T> setNamespace(String namespace) {
        this.namespace = namespace;
        return this;
    }    

    /**
     * Sets parameters for being passed through to the service method. Primitive
     * and complex types are supported, although complex types must have
     * implement the KvmSerializeble interface. Complex types must be annotated
     * by the SOAPObject annotation in order for the method calling be processed
     * correctly.
     *
     * @param params
     *            the parameters for being sent
     * @see SOAPObject
     * @see SOAPProperty
     * @see Wrapper
     */
    public Caller<T> setParams(LinkedHashMap<String, Object> params) {
        // adds the list of parameters
        for (String key : params.keySet()) {
            Object value = params.get(key);
            addParameter(key, value);
        }
        return this;
    }

    /**
     * Adds a parameter for being passed through to the service method.
     * Primitive and complex types are supported, although complex types must
     * have implement the KvmSerializeble interface. This could be achieved
     * through extending the Wrapper class. Additionally, complex types must
     * also be annotated by the SOAPObject annotation in order for the method
     * calling be processed correctly.
     *
     * @param name
     *            the parameter name
     * @param value
     *            the parameter value
     * @see SOAPObject
     * @see SOAPProperty
     * @see Wrapper
     */
    public Caller<T> addParameter(String name, Object value) {
        this.params.put(name, value);
        Class<?> type = value.getClass();
        SOAPObject soapObjectAnnotation = type.getAnnotation(SOAPObject.class);

        if(soapObjectAnnotation != null) {
            String typeId = soapObjectAnnotation.typeId();
            String namespace = soapObjectAnnotation.namespace();
            addMapping(type, typeId, namespace);
        }
        return this;
    }

    /**
     * Sets a parser for handling the service response.
     */
    public Caller<T> setResponseParser(IMethodResponseParser<T> parser) {
        this.responseParser = parser;
        return this;
    }    

    /**
     * Performs the service method call and grabs its response, passing it to
     * the IMethodResponseParser if it applies.
     *
     * @return the parsed data if a parser was provided, null otherwise.
     * @throws IllegalStateException
     *             if the RemoteServiceMethod isn't properly setup
     * @throws SoapFault
     * @throws IOException
     * @throws XmlPullParserException
     */
    public T call() throws IllegalStateException, IOException, XmlPullParserException, SoapFault {
        if(!isReadyForCall()) {
            throw new IllegalStateException("Invalid method configuration: make sure you setup all needed data for calling a SoapMethod");
        }

        String soapMethod = namespace + URL_SEPARATOR + serviceInterface + URL_SEPARATOR + method;
        SoapObject request = new SoapObject(namespace, method);
        if(params != null) {
            for (String key : params.keySet()) {
                Object value = params.get(key);
                PropertyInfo pi = new PropertyInfo();
                pi.name = key;
                pi.setType(value.getClass());

                // if there's a mapping for this class, it is a mapped complex type.
                // thus, we shall set it directly into the PropertyInfo, instead of
                // creating a SoapPrimitive object
                Mapping mapping;
                if((mapping = hasMapping(value.getClass())) != null) {
                    pi.setValue(value);
                    pi.setNamespace(mapping.namespace);
                } else {
                    // otherwise, we shall consider this a primitive value
                    pi.setValue(new SoapPrimitive(namespace, key, value.toString()));
                }

                request.addProperty(pi);
            }
        }

        SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
        envelope.dotNet = true;        envelope.setOutputSoapObject(request);

        for (Mapping mapping : mappings) {
            envelope.addMapping(mapping.namespace, mapping.typeId, mapping.type);
        }

        HttpTransportSE httpTransport = new HttpTransportSE(serverUrl.toString() + URL_SEPARATOR + service);
        httpTransport.call(soapMethod, envelope);
        SoapObject response = (SoapObject) envelope.getResponse();
        if(responseParser != null)
            return responseParser.parse(response);

        return null;
    }

    // Private Methods ----------------------------------------------------------------------------
    private boolean isReadyForCall() {
        return serverUrl != null
            && namespace != null
            && service != null
            && serviceInterface != null;
    }    

    private Mapping hasMapping(Class<?> klass) {
        for (Mapping mapping : mappings) {
            if(mapping.type == klass) {
                return mapping;
            }
        }
        return null;
    }    

    private Caller<T> addMapping(Class<?> type, String typeId, String namespace) {
        this.mappings.add(new Mapping(type, typeId, namespace));
        return this;
    }

    // Inner Classes ------------------------------------------------------------------------------
    private static class Mapping {
        private Class<?> type;
        private String typeId;
        private String namespace;

        public Mapping(Class<?> type, String typeId, String namespace) {
            super();
            this.type = type;
            this.typeId = typeId;
            this.namespace = namespace;
        }
    }
}

O código acima é bem extenso como vocês já devem ter notado. A ideia principal é seguir o padrão Builder, já que temos parâmetros demais para criar um método e além disso, muitos parâmetros são opcionais em certas situações, de forma que a adoção do design pattern fica mais do que apropriada (exceto pelo detalhe de que, seguindo o padrão à risca, duas classes deveriam ser usadas: a builder e a building). Seguir esse padrão implica em acumular dados para usar posteriormente, o que justifica o monte de atributos e até a classe interna Mapping. Nos resta o método call() que é quem realmente faz o trabalho pesado nessa classe.

O método call() interage diretamente com a API da biblioteca KSoap, baseando-se nos parâmetros configurados previamente e também nas anotações dos objetos, ele prepara e executa a chamada ao WebService propriamente dito. Ele adiciona parâmetros, mapeamentos (exigidos para que o KSoap consiga casar os tipos dos parâmetros passados com os tipos dos parâmetros do serviço), etc. Todo o processo está simplificado ai dentro, inclusive escondendo as nuances da API.

Um detalhe que é importante notar é que na parte 1 e 2 da série eu deixei sempre de lado um atributo importante da anotação @SoapObject: o namespace. Agora que estamos aqui, vale lembrar que é imprescindível que ele seja corretamente especificado nas classes que serão enviadas para o WS, caso da classe Client do exemplo. É baseado nessa anotação que o Caller vai gerar os mappings corretos para que o KSoap consiga enviar o request como especificado na interface do serviço.

E a Resposta?

Quem leu esse artigo até aqui certamente deve estar se fazendo essa pergunta. E se o serviço retornar dados, como faz? Esse caso está previsto nesse modelo através da interface IMethodResponseParser. É uma interface simples que tem apenas um método: parse(), que recebe um SoapObject e retorna o um tipo T genérico.

Com ela é possível transformar o SoapObject retornado pelo serviço em um objeto do modelo do seu aplicativo. No exemplo abaixo é criada uma instância anônima dessa interface, e a sua implementação mostra como receber uma lista de Clients do serviço.

import java.util.ArrayList;
import java.util.List;

import org.ksoap2.serialization.SoapObject;

import br.com.zbra.test.model.Client;
import br.com.zbra.test.soap.Caller;
import br.com.zbra.test.soap.IMethodResponseParser;
import br.com.zbra.test.soap.util.SoapUtils;

public class Service {
    private static final String SERVICE_CLIENT = "ClientService";
    private static final String METHOD_NAME = "GetClients";
    private static final String NAMESPACE = "http://zbra.com.br/ksoap";
    private static final String URL = "http://192.168.10.103/SpringTutorialService/SaveClientWS.asmx";

    public void getClients() {
       try {
		new Caller<List<Client>>()
			.setServerUrl(new java.net.URL(URL))
			.setNamespace(NAMESPACE)
			.setService(SERVICE_CLIENT)
			.setMethod(METHOD_NAME)
			.setResponseParser(new IMethodResponseParser<List<Client>>() {
				public List<Client> parse(SoapObject response) {
					ArrayList<Client> list = new ArrayList<Client>();

					for (int i = 0; i < response.getPropertyCount(); i++) {
						// get running application attributes
						SoapObject appSoap = (SoapObject)response.getProperty(i);
						Client app = SoapUtils.parseSoapObjectIntoAnnotatedVO(Client.class, appSoap);

						// adds to the list
						list.add(app);
					}

					return list;
				}
			})
			.call();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
    }
}

Conclusão

A utilização da abstração proposta torna bem mais fácil e menos suscetível a erros o consumo de Web Services através do KSoap. Ela também facilita a leitura do código e melhora drasticamente a manutenabilidade do mesmo.

Com esse artigo eu estou encerrando minha série de publicações sobre KSoap no Android. Ao longo desses vários posts vimos como automatizar o envio de objetos complexos, a transcrição da resposta em objetos do modelo da aplicação, e finalmente agora como encapsular as chamadas ao WS e evitar a duplicação de código perigoso. Espero que tenham se divertido tanto quanto eu.

What’s Next?

Ainda há muitas lacunas a serem cobertas nesse pequeno projeto de framework que venho apresentando aqui. Transcrição automática de coleções (listas, arrays, hashmaps), suporte a herança nas classes básicas, automatizar a transcrição das respostas, e muitas outras coisas que eu nem imagino.

Por hora, meu companheiro de trabalho e mentor intelectual da classe Caller, Fábio Falavinha, sugeriu a implementação de um proxy dinâmico que, dada uma interface. que deve ser um espelho da interface do Web Service, mais um par de anotações, gera o objeto Caller, faz a chamada ao WS e retorna o valor bonitinho, sem que você precise fazer nadinha.

Além disso, estamos disponibilizando o código sob Licença Apache 2.0. Fiquem a vontade para submeter patchs, melhorias, ou reportar problemas, bugs, enfim. Agora esse código é de domínio público, divirtam-se!

UPDATE: A sugestão nosso amigo Fabio Falavinha, de implementar um proxy dinâmico, se concretizou em forma de artigo e também já está integrada na nossa biblioteca! Não deixe de conferir!

6 Comentários

  • Reply

    Por Felipe Silvestre em 22 de November de 2011 às 11:36

    Bruno, Fantástico. Uma dúvida que eu tenho, eu posso utilizar essa forma para passar para o webservice um Array Ou um List dos meus objetos que herdam da classe Wrapper. Precisa fazer algum acréscimo de código?

    • Reply

      Por Bruno Vinicius em 22 de November de 2011 às 17:47

      Opa Felipe! Infelizmente nessa versão ainda não está suportando isso. É preciso fazer um código adicional para isso.
      Aqui nesse link tem várias dicas pra fazer isso com objetos complexos e simples. http://code.google.com/p/ksoap2-android/wiki/CodingTipsAndTricks
      Estou escrevendo UnitTests para nossa biblioteca e assim que acabar devo liberar a versão beta, em seguida vou adicionar suporte para esse caso lah. Fica de olho =)

  • Reply

    Por Eron Reis em 13 de July de 2012 às 15:49

    Bruno, otimo artigo!Bom, não sei se vocês chegaram a abordar,quando o serviço possui uma grande quantidade de métodos como eu faço para invocar somente um deles?
    Abração!

    • Reply

      Por Bruno Vinicius em 18 de July de 2012 às 11:35

      Não tem problema algum, Eron, a abordagem é a mesma.

  • Reply

    Por Franklyn de Quadros em 19 de July de 2012 às 16:44

    Parabéns Bruno, excelente sequencia de artigos. Tentei baixar no link disponibilizado o código fonte, mas não foi possível baixar, poderia disponibilizar um outro link para que eu possa fazer o download do mesmo?

  • Reply

    Por Roney em 26 de August de 2014 às 15:40

    Olá Bruno, parabéns pelo artigo.
    Estou iniciando no desenvolvimento Android e ele está sendo minha base para o trabalho com webservices.
    Uma dúvida: com a última versão do ksoap2 (a atual), me parece que a própria chamada do .call já faz o parsing, não sendo necessário a chamada ao .parseSoapObjectIntoAnnotatedVO(), isso procede?
    Porque eu gostaria que não o fizesse pois está lento para a quantidade de informação que obtenho, e gostaria de utilizar o parsing ‘manual’.
    Desde já agradeço.
    sds





Desenvolvido por hacklab/ com WordPress