sexta-feira, 25 de janeiro de 2013

Cliente android consumindo REST de forma transparente, fácil e rápida

Olá, hoje vamos falar sobre aplicativos android consumindo serviços. Embora simples, existem alguns caminhos de pedra a seguir se você quiser implementar a solução logo. Vamos mostrar uma pequena aplicação de exemplo de modo a melhor contextualizar a situação.

Para mostrar este exemplo, os pre-requisitos são os seguintes:

  • eclipse Java EE com os plugins de glassfish e de android instalados
  • android SDK versão 2.2 ou superior
  • Oracle java 6 ou openjdk
  • glassfish 3.1.1 ou superior como nossa implementação do JEE 6
  • mysql 5.1
  • Slackware64 13.37

Comecemos pelo ambiente. No Slackware, você deve utilizar o slackpkg para instalar o JDK. Entretanto, antes de usar o slackpkg pela primeira vez, você deve descomentar um e apenas um mirror do arquivo /etc/slackpkg/mirrors. Feito isso, o seguinte comando, executado como root, deverá instalar o JDK da Oracle automaticamente no seu sistema:

slackpkg install jdk

A instalação full do slackware (a melhor opção de instalação, sempre faça essa) já vem com mysql instalado. O que fica manual são os ajustes que devemos fazer.

Primeiro comente a linha 32 do arquivo /etc/rc.d/rc.mysqld. É a linha do --skip-networking, comentando-a as conexões JDBC irão funcionar. Execute em seguida os seguintes comandos:

mysql_install_db --user=mysql sh /etc/rc.d/rc.mysqld start

E por fim o script interativo que irá fazer perguntas e tornar a instalação "segura":

mysql_secure_installation

Conecte-se ao mysql com o usuário root. Vamos criar o banco para nossa aplicação:

mysql -u root -p

O código a seguir deve ser executado no terminal do mysql que aparece depois de fazer login:

create database mural;
grant all privileges on mural.* to mural@localhost identified by 'mural';
flush privileges;

Dê CTRL+D, faça agora login da seguinte maneira:

mysql -p -u mural mural

Entre com a senha criada anteriormente e rode no console o seguinte SQL:

create table mural(id integer not null primary key auto_increment, mensagem varchar(800) not null, data date not null);

O passo seguinte é baixar um eclipse JEE edition da página de downloads do eclipse e descompactar em algum lugar para usar:

Após abrir o programa e escolher um workspace, vá para o menu Help>Eclipse Marketplace... e na caixa de busca do market coloque glassfish e dê enter. Instale o Glassfish Java EE Application Server Plugin for Eclipse, deve ser o primeiro da lista:

Após instalado o plugin, vamos reiniciar o eclipse e enquanto isso baixamos o glassfish e o descompactamos em algum lugar fácil de encontrar. Pegue a versão Full Platform, de preferência a versão .zip, que é a versão não-fique-no-meu-caminho-edition, ;-)

Feche a tela de boas vindas e prossiga para a aba servers; vamos associar o glassfish que você já descompactou (não é?) com este eclipse. Na área vazia da aba servers, botão direito e New>Server>Glassfish>Glassfish 3.1 aperte em Next e a tela pedindo a pasta do glassfish surgirá. Selecione a pasta glassfish de dentro da pasta glassfish3, deve ficar mais ou menos assim:

Feito isto, Next mais uma vez e Finish. Caso você já tenha usado tomcat, o funcionamento do glassfish é bem similar, embora o glassfish tenha "mais super-poderes", por assim dizer. Já podemos criar o projeto do serviço do mural!

Vamos montar um projeto web com suporte a Servlet 3.0 que irá rodar no glassfish. Vá em File>New>Dynamic Web Project. Chame o projeto se mural (sim.. vamos fazer uma pequena aplicação de mural, mas não espere nada muito grandioso.).

Agora fazemos o ponto de entrada da aplicação REST, a classe App herdando de javax.ws.rs.core.Application

package sample.mural;

import java.util.Set;
import java.util.HashSet;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/service")
public class App extends Application {
	
	@Override
	public Set> getClasses() {
		Set> classes = new HashSet>();
		// TODO ainda vamos criar os Resources
		return classes;
	}
}

O servidor deverá escanear e publicar sua aplicação automaticamente. Vamos adicionar um Resource. Crie a classe MuralResource:

package sample.mural;

import javax.ws.rs.Path;

@Path("/mural")
public class MuralResource {
	// TODO acesso ao banco, publicação, etc.
}

Retorne à classe App e modifique-a para ficar assim:

package sample.mural;

import java.util.Set;
import java.util.HashSet;/*TreeSet*/

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/service")
public class App extends Application {
	
	@Override
	public Set<Class<?>> getClasses() {
		Set<Class<?>> classes = new HashSet<Class<?>>();
		classes.add(MuralResource.class);
		return classes;
	}
}

Agora vem a melhor parte: não precisamos recuperar diretamente o DataSource para fazermos nossas consultas ao banco de dados, tampouco gerenciar manualmente os acessos concorrentes. Tudo o que precisamos fazer é tornar o MuralResource um Enterprise Java Bean, ou EJB para os mais íntimos.

Antigamente, no tempo antes do tempo (2006 pra trás), EJB era motivo de ódio e de ranger de dentes; a especificação JEE era falha e deu inúmeras brechas que permitiram cada grande fornecedor fizesse uma implementação que prendesse a solução em seus próprios produtos, impedindo a promessa da portabilidade dos aplicativos Java se realizar.

Aí veio o Spring chutar-lhes o saco com sua forma sensacional de gerenciar as instâncias e a Microsoft com seu bafo quente de .NET no pescoço deles. Aí Oracle, IBM, Red Hat e outros pararam de frescura e especificaram o fabuloso JEE6, o me perdoe eu melhorei edition, :-).

Transformar uma classe qualquer em uma classe de EJB não poderia ser mais fácil. Segue como fica o MuralResource após modificarmos a mesma para ser um EJB com injeção automática de recursos provenientes do servidor:

package sample.mural;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

// Esta anotação faz este JAX-RS Resource ser um EJB
@Stateless
// Esta é do JAX-RS
@Path("/mural")
public class MuralResource {

	// Esta anotação só funciona porque isto é um EJB
	@Resource(name = "jdbc/mural")
	// Jamais fazemos lookup nisto, ele é "Injetado" pelo servidor
	private DataSource ds;

	// este método devolve o total de recados na URI /service/mural/count
	@GET
	@Path("/count")
	public String getMsgCount() throws Exception {
		return ""+YASQLUtil.getRecadosCount(ds);
	}

	// este devolve uma lista de recados. Veremos detalhes mais adiante
	@GET
	public RecadoContainer getRecados() throws Exception {
		return YASQLUtil.getRecados(ds, 1);
	}

	// este devolve um offset específico de recados
	// a URI toma, por exemplo o seguinte formato: /service/mural/3
	// em que "3" é o número da página mapeado para uma variável do método
	// e validado por uma expressão regular no Path
	@GET
	@Path("/{id: [1-9]\\d*}")
	public RecadoContainer getRecadosByPagina(@PathParam("pagina") int pagina)
			throws Exception {
		return YASQLUtil.getRecados(ds, pagina);
	}

	// cadastro de um novo recado. O objeto é recebido no corpo do POST
	// e montado automaticamente pelo servidor.
	@POST
	public String newRecado(Recado rec) throws Exception {
		return ""+YASQLUtil.newRecado(ds, rec);
	}
}

Parece bem mais funcional, não? Notem a sutileza da anotação @Stateless: ela informa ao container que ele deve gerenciar as instâncias dessa classe, bem como os acessos à mesma. Não obstante, ele ganha direito a ter um contexto próprio, também gerenciado pelo servidor, de modo que podemos Injetar dentro dele objetos que o servidor de aplicação disponibiliza para os aplicativos nele publicados. O DataSource é um desses objetos. Se você viu o tutorial com Tomcat, a diferença é evidente, até gritante. Vários outros objetos podem ser injetados, outros EJB's, Mail Sessions, Filas JMS e por aí vai.

Abaixo as outras classes utilitárias criadas para auxiliar nosso EJB.

YASQLUtil:

package sample.mural;

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ResourceBundle;

import javax.sql.DataSource;

/**
 * YASQLUtil - Yet Another SQL Util. Você com certeza um dia vai ter um. Talvez
 * tenha. Existem chances. Pode ser que sim, pode ser que não. Eu nego piamente
 * que você precise de algo assim algum dia!
 * 
 * @author sombriks
 * 
 */
public class YASQLUtil {

	private static final ResourceBundle b = ResourceBundle//
			.getBundle(YASQLUtil.class.getName());

	public static int getRecadosCount(DataSource ds) throws Exception {
		Connection cn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			cn = ds.getConnection();
			ps = cn.prepareStatement(b.getString("count"));
			rs = ps.executeQuery();
			if (rs.next())
				return rs.getInt(1);
		} finally {
			close(cn, ps, rs);
		}
		return 0;
	}

	public static int newRecado(DataSource ds, Recado rec) throws Exception {
		Connection cn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			cn = ds.getConnection();
			ps = cn.prepareStatement(b.getString("insert"),//
					PreparedStatement.RETURN_GENERATED_KEYS);
			ps.setString(1, rec.getMensagem());
			ps.setDate(2, new Date(System.currentTimeMillis()));
			ps.executeUpdate();
			rs = ps.getGeneratedKeys();
			if (rs.next())
				return rs.getInt(1);
		} finally {
			close(cn, ps, rs);
		}
		return 0;
	}

	public static RecadoContainer getRecados(DataSource ds, int pagina)
			throws Exception {
		RecadoContainer ret = new RecadoContainer();
		Connection cn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			cn = ds.getConnection();
			ps = cn.prepareStatement(b.getString("select"));
			ps.setInt(1, (pagina - 1) * 10);
			rs = ps.executeQuery();
			while (rs.next()) {
				Recado r = new Recado();
				r.setId(rs.getInt(1));
				r.setMensagem(rs.getString(2));
				r.setData(rs.getDate(3));
				ret.getRetorno().add(r);
			}
		} finally {
			close(cn, ps, rs);
		}
		return ret;
	}

	private static void close(Connection cn, PreparedStatement ps, ResultSet rs) {
		try {
			if (rs != null)
				rs.close();
			if (ps != null)
				ps.close();
			if (cn != null)
				cn.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}

Arquivo de propriedades (YASQLUtil.properties):

count = select count(id) from mural
insert = insert into mural (mensagem,data) values (?, ?)
select = select id,mensagem,data from mural order by id desc limit 10  offset ?

Recado:

package sample.mural;

import java.util.Date;

import javax.xml.bind.annotation.XmlType;

/**
 * simples pojo de transporte
 * 
 * @author sombriks
 *
 */
@XmlType
public class Recado {

	private int id;
	private String mensagem;
	private Date data;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getMensagem() {
		return mensagem;
	}

	public void setMensagem(String mensagem) {
		this.mensagem = mensagem;
	}

	public Date getData() {
		return data;
	}

	public void setData(Date data) {
		this.data = data;
	}

}

RecadoContainer:

package sample.mural;

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

import javax.xml.bind.annotation.XmlRootElement;

/**
 * classe usada para ser o RootElement
 * 
 * @author sombriks
 * 
 */
@XmlRootElement
public class RecadoContainer {

	private List retorno = new ArrayList();

	public List getRetorno() {
		return retorno;
	}

	public void setRetorno(List retorno) {
		this.retorno = retorno;
	}
}

Exceto pelo YASQLUtil, apinhado de código específico de JDBC, o resto não representa grandes desafios. A classe de Container tem um único propósito: servir de elemento Raíz, pois não podemos usar JAXB para devolver listas diretamente; ele exige um elemento raíz para melhor refletir a estrutura de documento, seja ele XML ou JSON.

Estamos quase prontos para executar o teste deste aplicativo. Devemos agora configurar o DataSource do lado do servidor.

De modo a fazermos nossa aplicação utilizar aquela base de dados criada no mysql lá no início de nossas atividades, precisaremos fazer o download do conector jdbc do mysql; o Slackware não empacota esse driver, logo vamos baixa-lo aqui:

Descompacte o zip, como de costume em um lugar conhecido e fácil de achar. Dentro dele existe um .jar chamado mysql-connector-java-5.1.18-bin.jar, este é o driver do mysql.

Copie este driver para a pasta lib do glassfish. Especificamente a pasta lib que fica dentro do diretório glassfish dentro do diretório do glassfish... falamos disso lá em cima. Se ficar confuso, jogue o .jar do driver dentro da pasta lib dentro da pasta domain1 (exemplo: /home/sombriks/Downloads/glassfish3/glassfish/domains/domain1/lib). Você consegue, eu acredito em você!

Finalmente, você pode rodar o glassfish, pois vamos precisar dele no ar para criar o DataSource. Botão direito sobre o servidor do glassfish na aba servers, opção Start; aguarde o servidor subir totalmente (leva em torno de 15 segundos, até menos) e novamente com botão direito vá em GlassFish>View Admin Console. Se por algum motivo não funcionar, abra o endereço http://localhost:4848/ Diretamente no firefox:

Após a tela de carregamento desaparecer, uma barra lateral cheia de opções aparecerá. Selecione Resources>JDBC>JDBC Connection Pools. Uma tabela surgirá.

O Connection Pool é a parte que "conhece" o banco de dados. Nele é que vão os dados sensíveis do seu ambiente. Selecione o botão "New..." e comecemos a preencher os dados no formulário que surgirá:

  1. Pool Name: muralPool
  2. Resource Type: javax.sql.DataSource
  3. Database Driver Vendor: MySql (e não encoste na caixa de texto vazia abaixo!)

Aperte no botão next, localizado no canto superior direto (WHY???), na tela que surgirá desca até a tabela "Additional Properties (203)" e dentre as centenas de atributos, encontre e informe estes:

  1. User: mural
  2. ServerName: localhost
  3. DatabaseName: mural
  4. Password: mural
  5. Url: jdbc:mysql://localhost:3306/mural
  6. URL: jdbc:mysql://localhost:3306/mural (tem duas, ajuste ambas só por via das dúvidas)

Pressione finish quando acabar. O passo seguinte é Resources>JDBC>JDBC Resources, bem mais simples. Pressione New... e forneça os seguintes dados no formulário:

  1. JNDI Name: jdbc/mural (o mesmo nome usado na anotação @Resoruce, lá no ejb dentro da aplicação)
  2. Pool Name: muralPool

Após pressionar OK, o servidor terá um DataSource publicado pronto para ser consumido. Tudo isso para que o desenvolvedor da aplicação não tenha a necessidade de saber muitos detalhes sobre o banco de dados. Sim... de alguma forma, isso é uma coisa boa, mas deixemos este debate filosófico para outro momento.

Já viemos até aqui... Podemos iniciar a aplicação, só pra ver qualé. Reinicie o glassfish, quando ele voltar, aperte o botão direito sobre o projeto do eclipse, Run As>Run on Server. Na tela que surge, selecione o glassfish e pressione Finish. No console a saída deve ser mais ou menos assim:

INFO: Portable JNDI names for EJB MuralResource : [java:global/mural/MuralResource!sample.mural.MuralResource, java:global/mural/MuralResource] INFO: Registering the Jersey servlet application, named sample.mural.App, at the servlet mapping, /service/*, with the Application class of the same name INFO: WEB0671: Loading application [mural] at [/mural] INFO: mural was successfully deployed in 6,745 milliseconds.

Visite http://localhost:8080/mural/service/mural/count e veja a contagem de zero mensagens, ;-)

O passo seguinte é o lado android da solução. Comecemos com o download do Android SDK:

Como já fizemos anteriormente, descompacte em lugar conhecido. Vamos instalar o plugin de eclipse para desenvolver para android, o ADT Plugin for Eclipse:

Basta seguir os passos recomendados na página. De forma resumida, copie a url da caixa em destaque, vá para o eclipse, menu Help>Install New Software>Add... e cole a url no campo Name e no campo Location. pressione Ok e aguarde, o eclipse começará a recuperar as informações do plugin. Quando aparecer a opção Developer Tools, marque o checkbox da mesma e aperte o botão Next e vá apertando Next e aceitando termos de licença e essas coisas menores até o plugin finalmente começar a ser instalado. Quando o eclipse pedir para reiniciar, reinicie o eclipse; por ser "primeiro acesso" ele voltará perguntando onde foi que você descompactou o SDK. Indique o caminho completo selecionando a opção "Use existing SDKs". Após indicar a pasta ele apresentará um alerta, mas você pode ignorar e ir adiante.

E finalmente podemos desenvolver android, certo? errado!

O sistema é 64 bits, e o google só disponibiliza a sdk em 32 bits! Erros estranhos e sem sentido como o descrito abaixo vão aparecer no console do eclipse:

[2011-12-13 18:35:16 - DDMS] DDMS files not found: /home/sombriks/Downloads/eclipse/platform-tools/adb /home/sombriks/Downloads/eclipse/tools/hprof-conv /home/sombriks/Downloads/eclipse/tools/traceview [2011-12-13 18:38:54 - DDMS] DDMS files not found: /home/sombriks/Downloads/android-sdk-linux/platform-tools/adb

Embora erro primário, no slackware você corrige facilmente: baixe o pacote projetado pelo alienBOB e disponibilizado pela comunidade italiana do slacwkare: http://repository.slacky.eu/slackware64-13.37/libraries/compat32-libraries/

Neste link tem uma pasta que dentro dela tem o pacote .txz das bibliotecas de compatibilidade. Faça o download e após isso instale o pacote com o seguinte comando (supondo terminal no mesmo diretório do pacote):

installpkg compat32-libraries-*.txz

Terminada a instalação, feche o eclipse e abra-o novamente. Agora ele virá sem erros... Errado! ainda há um alerta surgindo na tela:

Siga as intruções do alerta, abra o SDK Manager. Caso esteja sem saber o que vem a ser o SDK Manager, você chega nele através daquele botão na toolbar que tem uma caixa com uma seta para baixo, e um robôzinho verde dentro, ;-). Você verá esse ícone ampliando o screenshot anterior. Essa é a tela do SDK Manager:

Marque o checkbox das Tools, e opcionalmente baixe outras versões da SDK do android além da que já se encontra marcada. No meu caso, Baixarei a SDK, Samples e Google API's da versão 2.2:

Pressione o botão Install, que se você observar está contanto os pacotes que você marcou... como se fosse da conta dele!

Depois de um longo tempo fazendo downloads, quando ele acabar feche o SDK Manager, reinicie o eclipse e finalmente criaremos o projeto android:

Batize o projeto como muraldroid, selecione a versão da SDK que desejar (irei deixar a 4.0 marcada pois pode ser que você só tenha essa) e no Package Name coloque sample.android.mural e pressione Finish.

Seu workspace deve estar assim, com os dois projetos nele:

Vamos abrir a pasta res/layout e editar o main.xml, que é o content view da Activity que foi criada automaticamente. Ele deve estar mais ou menos assim:




    


Troque por este conteúdo aqui:




    

        
    

    

Crie ainda num novo android xml chamado simpletext.xml, pois iremos precisar dele para renderizar o label da lista de recados:




O android deverá automaticamente renderizar a classe R na pasta gen contento este novo id: simpletext.

Agora vamos para a classe da nossa Activity, a MuraldroidActivity. Vamos adicionar o código que recupera a ListView que adicionamos no leiaute principal e iremos usar um Adapter estático, apenas para vermos a lista e o scroll em ação:

package sample.android.mural;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MuraldroidActivity extends Activity {
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		ListView listView = (ListView) findViewById(R.id.listView1);
		ArrayAdapter adapter = //
		new ArrayAdapter(this, R.layout.simpletext);
		int i = 30;
		while (i-- > 0)
			adapter.add("item " + i);
		listView.setAdapter(adapter);
		adapter.notifyDataSetChanged();

	}
}

Agora com o botão direito sobre o projeto selecione Run As>Android Application; o eclipse vai lhe perguntar se você deseja criar um novo dispositivo; responda Yes e caso você não tenha um aparelho no modo debug por perto, crie um virtual.

Após criar o dispositivo virtual, ele irá carregar... não feche mais o emulador, o segundo "deploy" será mais rápido com o emulador já rodando.

Supondo que você tenha obtido sucesso rodando pelo emulador, algo do tipo deve aparecer:

Legal, né? Vamos agora configurar o AndroidManifest.xml para liberarmos acesso à internet para nossa aplicação. Deixe-o assim:




    
    

    
        
            
                

                
            
        
    


Crie uma pequena classe auxiliar, vamos chamá-la de ClienteServico. Ela irá fazer uma requisição http para o serviço que temos rodando no glassfish e dessa forma conectar os dois lados do projeto. Ela será um singleton, você pode faze-la da seguinte maneira:

package sample.android.mural;

import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.DefaultHttpClient;

public enum ClienteServico {
	INSTANCE;

	// NOTA: troque 192.168.0.193 pelo IP da rede interna do seu computador
	// pois o emulador não está no localhost!
	private String list = "http://192.168.0.193:8080/mural/service/p%d";
	private String save = "http://192.168.0.193:8080/mural/service";

	// Recado e RecadoContainer foram copiados do projeto do serviço.
	// Apague a anotação do @XmlRootElement da classe RecadoContainer
	// Mude o tipo do campo data de Date para String na classe Recado
	public RecadoContainer getRecadosByPagina(int page) throws Exception {
		DefaultHttpClient client = new DefaultHttpClient();
		HttpUriRequest req = new HttpGet(String.format(list, page));
		req.addHeader("Accept", "application/json");// PROBLEM
		// TODO terminar
		return null;
	}

	public String newRecado(Recado rec) throws Exception {
		DefaultHttpClient client = new DefaultHttpClient();
		HttpUriRequest req = new HttpPost(save);
		req.addHeader("Accept", "plain/text");
		// TODO terminar
		return null;
	}
}

Este é o ponto em que eu finalmente queria chegar. Aqui, o caminho a seguir seria baixar o Gson e colocar como dependência do projeto android. Entretanto, alguns problemas de ordem prática vão ocorrer que nos levarão a não usar essa biblioteca. Considere o json válido abaixo:

{"retorno":[
	{"data":"2011-12-10T00:00:00-03:00","id":"1","mensagem":"olá pessoas!"},
	{"data":"2011-12-10T00:00:00-03:00","id":"2","mensagem":"como estão vocês?"},
	{"data":"2011-12-10T00:00:00-03:00","id":"3","mensagem":"recado rápido!"}
]}

Isto é o retorno gerado pelo serviço REST do glassfish quando pedimos a ele a lista de recados. Não precisa acreditar em mim, você pode dar carga manualmente no banco e requisitar ao serviço este JSON:

Carga no banco:

insert into mural (mensagem,data) values ('olá pessoas!','2011-12-10');
insert into mural (mensagem,data) values ('como estão vocês?','2011-12-10');
insert into mural (mensagem,data) values ('recado rápido!','2011-12-10');

Comando rápido para requisitar JSON no terminal do slackware:

curl -H "Accept:application/json" -X GET http://localhost:8080/mural/service/mural

Se quiser que seja entregue XML:

curl -H "Accept:text/xml" -X GET http://localhost:8080/mural/service/mural

Veja como é a saída xml:



	
		2011-12-10T00:00:00-03:00
		1
		ola pessoas!
	
	
		2011-12-10T00:00:00-03:00
		2
		como estao voces?
	
	
		2011-12-10T00:00:00-03:00
		3
		recado rápido!
	

Notaram a diferença? O elemento que contém a lista, na versão XML está implícito. Já na versão JSON, ele se chama retorno e tem uma lista válida, tudo feliz e normal.

Agora delete dois dos três registros do banco:

delete from mural where id in (1,2);

Feito isso, use novamente o curl para recuperar o XML... e recupere em seguida o JSON. As saídas serão as seguintes:



	
		2011-12-10T00:00:00-03:00
		3
		recado rápido!
	

{"retorno":
	{"data":"2011-12-10T00:00:00-03:00","id":"3","mensagem":"recado rápido!"}
}

A diferença é sutil, porém decisiva. A versão JSON perdeu a característica da lista; sim, poderíamos considerar que a lista tornou-se tão implícita quanto a versão XML, mas é fácil provar que esta atual versão JSON não tem condições de diferenciar entre um elemento único e uma lista de elementos, como outrora era claro. Curto e grosso: a versão JSON perde informação nestes casos específicos.

Mas este não é o problema maior.

Sua aplicação android É escrita em java, e em java a definição dos atributos é estática. Poderíamos utilizar saídas do próprio JAXB, mas não existe JAXB para android; se quiser adiciona-lo manualmente, serão pouco mais de nove mega de download e aproximadamente 5 mega de dependências... talvez menos, mas certamente será maior que toda a sua aplicação. Diversão mesmo é tentar usar o JAXB no android, tem um easter egg esperando os mais ousados, mas não é agradável.

Por isso, embora tentador, não use json caso você não tenha tempo de escrever um parser customizado e deseja mesmo converter a stream de forma transparente de e para o serviço. A biblioteca simplexml e o header HTTP ajustado para receber XML farão o serviço funcionar muito mais rapidamente.

Modifique o ClienteServico para ficar assim:

package sample.android.mural;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;

public enum ClienteServico {
	INSTANCE;
	// NOTA: troque 192.168.0.193 pelo IP da rede interna do seu computador
	// pois o emulador não está no localhost!
	private String list = "http://192.168.0.193:8080/mural/service/p%d";
	private String save = "http://192.168.0.193:8080/mural/service";

	// Recado e RecadoContainer foram copiados do projeto do serviço.
	// Apague a anotação do @XmlRootElement da classe RecadoContainer
	public RecadoContainer getRecadosByPagina(int page) throws Exception {
		DefaultHttpClient client = new DefaultHttpClient();
		HttpUriRequest req = new HttpGet(String.format(list, page));
		req.addHeader("Accept", "text/xml");
		HttpResponse res = client.execute(req);
		InputStream in = res.getEntity().getContent();
		Serializer ser = new Persister();
		RecadoContainer c = ser.read(RecadoContainer.class, in);
		in.close();
		return c;
	}

	public String newRecado(Recado rec) throws Exception {
		Serializer ser = new Persister();
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ser.write(rec, baos);
		DefaultHttpClient client = new DefaultHttpClient();
		HttpPost req = new HttpPost(save);
		req.setEntity(new StringEntity(new String(baos.toByteArray(), "UTF-8")));
		req.addHeader("Content-type", "text/xml");
		req.addHeader("Accept", "plain/text");
		HttpResponse res = client.execute(req);
		InputStream in = res.getEntity().getContent();
		int i = -1;
		String ret = "";
		byte[] buf = new byte[1024];
		while ((i = in.read(buf)) > -1)
			ret += new String(buf, 0, i);
		return ret;
	}
}

E a nossa activity deve fazer uso destas chamadas de serviço. Modifique-a para ficar assim:

package sample.android.mural;

import java.util.List;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;

public class MuraldroidActivity extends Activity {

	private ListView mensagensView;
	private EditText txtMensagem;
	private Button buttonPostar;
	private RecadoListAdapter listAdapter;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		// referências
		mensagensView = (ListView) findViewById(R.id.listView1);
		txtMensagem = (EditText) findViewById(R.id.editText1);
		buttonPostar = (Button) findViewById(R.id.button1);
		// eventos e modelos
		buttonPostar.setOnClickListener(new OnClickListener() {
			public void onClick(View v) {
				salvarRecado();
				listAdapter.clear();
				updateMural();
			}
		});
		listAdapter = new RecadoListAdapter(this);
		mensagensView.setAdapter(listAdapter);
		updateMural();
	}

	private void updateMural() {
		AsyncTask> at = //
		new AsyncTask>() {
			@Override
			protected List doInBackground(Void... params) {
				try {
					return ClienteServico.INSTANCE//
							.getRecadosByPagina(1).getRetorno();
				} catch (Exception e) {
					e.printStackTrace();
				}
				return null;
			}
		}.execute();
		try {
			listAdapter.addRecados(at.get());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/*
	 * utilitário para salvar os recados
	 */
	private void salvarRecado() {
		AsyncTask at = //
		new AsyncTask() {
			@Override
			protected String doInBackground(Void... params) {
				Recado r = new Recado();
				r.setMensagem(txtMensagem.getText().toString());
				String s = null;
				try {
					s = ClienteServico.INSTANCE.newRecado(r);
				} catch (Exception e) {
					e.printStackTrace();
				}
				return s;
			}

		}.execute();
		try {
			Toast.makeText(MuraldroidActivity.this,
					"Mensagem " + at.get() + " cadastrada", //
					Toast.LENGTH_SHORT).show();
		} catch (Exception e1) {
			e1.printStackTrace();
		}
	}
}

Observem o uso do AsyncTask para fazer as requisições ao serviço: a partir da versão 3 do android, você não pode mais fazer operações de IO blocante na thread principal e nem pode modificar a interface gráfica a partir de threads que não sejam a principal. Daí o uso de AsyncTask.

E abaixo os beans de domínio com os ajustes para escapar dos problemas com conversão de data e com as anotações do simplexml.

Classe Recado:

package sample.android.mural;

import org.simpleframework.xml.Default;

/**
 * simples pojo de transporte
 * 
 * @author sombriks
 *
 */
@Default(required=false)
public class Recado {

	private int id;
	private String mensagem;
	private String data;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getMensagem() {
		return mensagem;
	}

	public void setMensagem(String mensagem) {
		this.mensagem = mensagem;
	}

	public String getData() {
		return data;
	}

	public void setData(String data) {
		this.data = data;
	}

}

Classe RecadoContainer:

package sample.android.mural;

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

import org.simpleframework.xml.Default;
import org.simpleframework.xml.ElementList;

/**
 * 
 * @author sombriks
 * 
 */
@Default(required=false)
public class RecadoContainer {

	@ElementList(inline = true, entry = "retorno", required = false)
	private List retorno = new ArrayList();

	public List getRetorno() {
		return retorno;
	}

	public void setRetorno(List retorno) {
		this.retorno = retorno;
	}
}

Sem esquecer do ListAdapter que implementamos para a ocasião:

package sample.android.mural;

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

import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

public class RecadoListAdapter extends BaseAdapter {

	private List recados = new ArrayList();
	private LayoutInflater inflater;

	public RecadoListAdapter(Activity ctx) {
		inflater = (LayoutInflater) ctx
				.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}

	public void clear() {
		recados.removeAll(recados);
		notifyDataSetChanged();
	}

	public void addRecados(List recados) {
		this.recados.addAll(recados);
		notifyDataSetChanged();
	}

	@Override
	public int getCount() {
		return recados.size();
	}

	@Override
	public Object getItem(int position) {
		return recados.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		Recado r = recados.get(position);
		System.out.println(r.getData());
		if (convertView == null)
			convertView = inflater.inflate(R.layout.simpletext, null);
		TextView t = (TextView) convertView;
		System.out.println(t);
		t.setText(r.getMensagem());
		return convertView;
	}
}

Sem mais, esta é uma boa abordagem para o consumo de serviços REST, sinta-se à vontade para testar e variar detalhes disso.

E boa sorte!

Nenhum comentário :

Postar um comentário