Estabelecendo conexões HTTP com J2ME

Autor : Roger Pedroso
Email:
roger_pedroso@ig.com.br

O desenvolvimento de aplicações para dispositivos móveis tem crescido de maneira vertiginosa. Poder construir soluções que estejam disponíveis para o usuário onde quer que ele esteja representa uma considerável possibilidade de expansão para o mercado de software. 

Dentre as tecnologias existentes endereçadas para a construção de aplicações wireless, o J2ME (Java 2 Platform, Micro Edition) é a que, certamente, pode ser encontrada mais facilmente em telefones celulares.  

Dada a necessidade de atender uma gama de dispositivos com recursos bem variados, o J2ME foi dividido em duas configurações. Uma configuração é responsável por fornecer uma máquina virtual Java e um conjunto básico de bibliotecas. A configuração CDC (Connected Device Configuration) é indicada para equipamentos com recursos mais ricos, como PDAs high-end. Já a configuração CLDC (Connected Limited Device Configuration) é destinada a equipamentos com recursos mais restritos. É ela que está disponível nos telefones celulares. 

A arquitetura J2ME foi projetada prevendo, também, a construção de um conjunto de bibliotecas de mais alto nível a partir das bibliotecas de uma configuração. Isto recebeu o nome de perfil (Profile, em inglês). Para aparelhos celulares foi definido o padrão MIDP (Mobile Information Device Profile). 

O objetivo deste artigo é demonstrar como fazer a comunicação via HTTP entre um dispositivo móvel que implemente o MIDP do J2ME e outros computadores. 

Comunicação entre equipamentos 

Uma das primeiras preocupações que surgem quando se inicia o desenvolvimento de, praticamente, qualquer tipo de aplicativo é como acontecerá a comunicação com outros sistemas. Isto vale para aplicações corporativas e, até mesmo, para jogos. Neste último caso, mesmo que não se esteja falando de um produto multi-player, a capacidade de trocar informações com um servidor pode ser necessária para o aumento do valor oferecido aos usuários. 

Quando se pensa em aplicações corporativas, a conectividade se torna uma questão bem mais crítica. De maneira geral, o programa executado no dispositivo faz parte de um sistema maior.  Este sistema possui interfaces específicas para cada classe de equipamento a ser utilizado. Neste contexto, de acordo com sua natureza a atividade é direcionada para a classe de equipamento mais apropriada. 

Tomemos como exemplo uma aplicação para o gerenciamento das atividades dos consultores de uma empresa. Cabe à interface acessada via desktop a entrada dos dados mais volumosos. É através dela que serão cadastradas as informações do consultor, do cliente, da visita a ser feita e do trabalho a ser executado. Utilizando a interface para telefone celular, o consultor irá receber estes dados e apenas sinalizará o andamento de cada atividade. 

Na versão standard do Java as funcionalidades de comunicação são encontradas nos pacotes java.net e java.io.  Evidentemente, as restrições impostas pelos, ainda limitados, recursos dos dispositivos móveis não permitem que sejam disponibilizadas todas as facilidades presentes nestes pacotes. Em seu lugar, na configuração CLDC do J2ME pode ser encontrado um framework de conexão denominado Generic Connection Framework (GCF). 

O Generic Connection Framework 

O GCF está presente no pacote javax.microedition.io. Ele é formado por uma hierarquia de interfaces e classes que implementam o pattern factory. Na base da hierarquia de interfaces está Connection. A partir dela são derivadas outras interfaces para atender necessidades e protocolos específicos. 

A maneira de criar um objeto que implemente a interface Connection é através do método estático open da classe Connector. Este método recebe como parâmetro uma URL, que indica o recurso ao qual se deseja conectar. 

Abaixo é apresentada a estrutura de uma URL e o significado de cada elemento. 

scheme://user:password@host:port/url-path;parameters 

scheme: Protocolo a ser usado na comunicação. Ex: http;

user: Identificação do usuário (opcional);

password: Senha do usuário (opcional);

host: Nome ou IP do host onde o recurso está localizado;

port:Porta do host a ser acessada (opcional);

url-path:Caminho para o recurso;

parameters:Parâmetros a serem passados ao recurso (opcional). 

O GCF foi projetado para ser facilmente expandido. Isto é especialmente importante se for considerada a natureza dos protocolos. Protocolos não são genéricos e sempre possuem características bem específicas. Logo é imprescindível que um framework deste tipo seja expansível para acomodar mais facilmente novas formas de comunicação. 

No MIDP, desde sua versão 1.0, é definida uma nova interface a partir de Connection para tratar das especificidades do protocolo HTTP: HttpConnection. 

Construindo requisições HTTP 

A maneira mais apropriada de fazer a conexão entre uma aplicação J2ME e uma outra aplicação externa é através da interface HttpConnection. Por intermédio desta interface é possível provocar a execução de um programa em um servidor HTTP e receber o resultado disto. 

O programa a ser executado tanto pode ser um já existente, contendo regras de negócio de uma aplicação em camadas, quanto um construído especificamente para recuperar informações de SGBD e retorná-las ao aplicativo J2ME. 

Evidentemente, este programa não precisa ser escrito, necessariamente, em Java. Na verdade, pode ser utilizada qualquer tecnologia capaz de tratar requisições HTTP, como PHP ou .NET. Entretanto, neste artigo será exemplificada a comunicação entre J2ME e um Servlet em um servidor J2EE. 

Mais adiante serão apresentadas partes do código de uma aplicação utilizada por um consultor para recuperar a relação de atividades que deve executar em uma visita a um cliente. 

O primeiro ponto que deve ser observado ao estabelecer conexões é que as mesmas podem consumir muito tempo. Sendo assim, é importante sempre dar ao usuário a opção de não ficar aguardando sua conclusão. Este efeito é obtido obedecendo ao princípio de sempre realizar conexões em threads separadas. 

Para atender este requisito, neste exemplo existe uma classe (LoadTask) que estende Thread. É ela que realmente recupera a lista de atividades. 

A listagem 1 exibe parte do código do programa que utiliza esta classe. 

Listagem 1

 LoadTask loader = new LoadTask();
loader.setParam(idUsuario,idCliente);
loader.setLoadListener(this);
loader.start();
showWaintingTasks(); 

 Na aplicação J2ME, quando o usuário executa o comando relativo à atualização da lista de atividades, é criada uma instância de LoadTask.  Em seguida, é passado para este objeto, através do método setParam, a identificação do consultor e do cliente que ele irá visitar. 

O método setLoadListener é utilizado para que o programa se registre como um listener do processo de atualização das atividades, que acontecerá na outra thread. Para tanto, é necessário que ele, além de estender a classe Midlet, também implementa a interface LoadListener.  

Na listagem 2 é apresentada a estrutura da interface LoadListener. 

Listagem 2

import java.util.Vector;
public interface LoadListener {
    public void loadFinished(Vector list);
}

 Esta interface define apenas o método loadFinished. Este método será executado em cada listener registrado na instância de LoadTask. Ele tem como parâmetro a lista de atividades recuperada. É desta maneira que a thread secundária devolve para a thread principal as informações obtidas através da conexão HTTP. 

Retornando à listagem 1, ao ser executado o método start é que a thread secundária será criada. A principal, então, continuará sua execução, chamando o método showWaintingTasks.  

O método showWaintingTasks mostra uma nova tela ao usuário, que indica que dados estão sendo atualizados. Nesta tela há um comando de Cancelar que, se acionado, provoca a exibição da tela principal da aplicação. Desta maneira, o usuário tem a possibilidade de não aguardar o final da execução da thread secundária e pode continuar a executar outra parte da aplicação, se desejar . 

Agora será analisado o que acontece na thread criada através da execução do método start na instância da classe LoadTask. A listagem 3 mostra a estrutura desta classe. 

Listagem 3

import java.io.*;
import javax.microedition.io.*;

import java.util.Vector; 

public class LoadTask extends Thread {    

    private Vector listeners = new Vector();
    private int idUsuario, idCliente;
    public void setLoadListener(LoadListener listener) {
        listeners.addElement(listener);
   

    public void setParam(int idUsuario, int idCliente) {
       
this.idUsuario = idUsuario;
       this.idCliente = idCliente;
   
}    

    public void run() {
        Vector tasks = new Vector();
        try {
            HttpConnection conn = (HttpConnection)
            Connector.open(
              "http://localhost:8080/fieldcontrol/gettask");
            conn.setRequestMethod(HttpConnection.POST);           
            conn.setRequestProperty("Content-Type",

                       "application/x-www-form-urlencoded");
            conn.setRequestProperty(
      "User-Agent","Profile/MIDP-1.0 Configuration/CLDC-1.0"
             );
            conn.setRequestProperty(
                              "Content-Language", "pt-BR");
            conn.setRequestProperty("Accept",
                               "application/octet-stream");
            conn.setRequestProperty("Connection", "close");
            String formData = "idUsuario=" + idUsuario +
                              "&idCliente=" + idCliente;
            byte[] data = formData.getBytes();
            conn.setRequestProperty("Content-Length",
                          Integer.toString(data.length));
            OutputStream os = conn.openOutputStream();
            os.write(data);
            os.close();
            int rc = conn.getResponseCode();
            if (rc == HttpConnection.HTTP_OK) {
                DataInputStream dis = new
                    DataInputStream(conn.openInputStream());
                while (dis.available() > 0) {
                    Task task = new Task();
                    task.setIdTask(dis.readInt());
                    task.setDescr(dis.readUTF());
                    task.setStatus(dis.readInt());
                    tasks.addElement(task);
                }
                dis.close();
            }
        }
        catch(Exception rse) {
            rse.printStackTrace();
        }
        if (listeners != null) {
            for (int i = 0; i < listeners.size(); i++) {
                    LoadListener listener = (LoadListener)
                                     listeners.elementAt(i);
                    listener.loadFinished(tasks);
           
}
        }
    }
}

 

 A thread começa sua execução no método run e é nele que realmente é feita a conexão HTTP. 

O protocolo HTTP segue um modelo de requisição-resposta. Cada requisição possui um conjunto de cabeçalhos que fornecem diferentes informações sobre ela. Estes cabeçalhos devem ter seus valores definidos antes que a requisição seja enviada. A interface HttpConnection fornece métodos para atribuir valores a eles. 

O corpo do método run inicia com a utilização o método open do factory Connector para criar um objeto que implementa HttpConnection. Neste momento a requisição ainda não foi enviada e encontra-se no estado de Setup. 

Na sequência, através do método setRequestMethod, é definido qual o método HTTP que será utilizado. Dos vários métodos definidos para este protocolo, o MIDP suporta apenas GET e POST.  

Ambos os métodos passam informações através da URL de conexão e de vários cabeçalhos. O que difere ambos é a maneira como dados complementares são enviados. No exemplo, estes dados complementares são a identificação do consultor e do cliente a visitar. 

No método GET, dados deste tipo são passados como parâmetros na URL. No método POST eles são enviados através de uma stream de saída, que é criada depois de os cabeçalhos terem sido definidos. Como existe uma restrição para a quantidade de caracteres na URL, é interessante adotar como padrão o método POST. Além disso, a passagem de conteúdo binário só é possível através deste método. 

Em seguida, o método setRequestProperty é executado várias vezes para definir o valor de alguns cabeçalhos HTTP. 

O cabeçalho Content-Type indica o tipo MIME do conteúdo enviado que não faz parte nem da URL e nem dos cabeçalhos. O tipo MIME application/x-www-form-urlencoded indica que o conteúdo extra está na forma de pares nome-valor. 

O cabeçalho User-Agent é usado para indicar que tipo de cliente está enviando a requisição HTTP. Aqui, se o cliente fosse um browser WEB o valor poderia ser “Mozilla/4.0”.  Para aplicações J2ME o usual é identificar o cliente como “Profile/MIDP-1.0 Configuration/CLDC-1.0”. 

O cabeçalho Accept indica o tipo MIME que o cliente aceita para as informações que forem devolvidas para ele. O valor application/octet-stream significa que o cliente espera receber a resposta num formato binário de dados. Neste caso, o cliente tem que saber exatamente como interpretar o conjunto de bytes recebidos, que não estão num formato padronizado. 

O objetivo do cabeçalho Connection com o valor close é informar ao servidor que não há a intenção de fazer novas requisições logo em seguida. Isto faz com que a conexão seja realmente fechada, assim que a requisição corrente for atendida. 

Apesar de o protocolo HTTP não exigir que um valor para o cabeçalho Content-Length seja definido, é importante fazer isto sempre que possível. Este cabeçalho indica a quantidade de dados complementares que serão enviados na requisição. 

No exemplo, é construída uma string com a identificação do consultor e do cliente a visitar (no formato nome-valor) e, então, é extraída dela um array de bytes. Este array é utilizado, inicialmente, para definir o valor do cabeçalho Content-Length. Feito isto, através do método openOutputStream da HttpConnection, é obtida uma stream de saída e nela é gravado o conteúdo do array de bytes. Ao ser fechada esta stream, a requisição é enviada ao servidor. 

Atendendo requisições

 Uma vez que o cliente J2ME envia a requisição, uma aplicação do lado servidor irá recebê-la, interpretá-la e gerar uma resposta.

 A listagem 4 mostra parte do código de um Servlet feito para atender requisições como a do exemplo. 

Listagem 4

 protected void doPost(HttpServletRequest request,
                      HttpServletResponse response)
                     
throws ServletException, IOException {

    String usuario = request.getParameter("idUsuario");
    String cliente = request.getParameter("idCliente");
    List tasks = getTasks(usuario, cliente);
    ByteArrayOutputStream baos =
                               new ByteArrayOutputStream();
   
DataOutputStream dos = new DataOutputStream(baos);
    for (Iterator i = tasks.iterator(); i.hasNext(); ) {
        Task task = (Task) i.next();
        dos.writeInt(task.getIdTask());
        dos.writeUTF(task.getDescr());
        dos.writeInt(task.getStatus());
    }
    byte[] data = baos.toByteArray();
    dos.close();
    baos.close();
    response.setStatus(HttpServletResponse.SC_OK);
   
response.setContentLength(data.length);
    response.setContentType("application/octet-stream");
    OutputStream os = response.getOutputStream();
    os.write(data);
    os.close();
}

 

 Neste caso, inicialmente são recuperadas as identificações do consultor e do cliente. Estas informações são passadas ao método getTasks, que retorna uma lista de atividades (Task). Cada atividade possui os atributos identificação (tipo int), descrição (tipo String) e status (tipo int). 

É criada, então, uma instância de ByteArrayOutputStream e, a partir dela, uma instância de DataOutputStream. ByteArrayOutputStream implementa uma stream de saída onde os dados são gravados em um array de bytes. DataOutputStream permite que tipos primitivos sejam escritos em uma stream de maneira portável, oferecendo uma maneira de recuperá-los facilmente de volta através de DataInputStream. 

Na sequência, cada atividade da lista é recuperada em um laço e seus atributos são gravados na DataOutputStream, usando o método apropriado para cada tipo. 

Após processar todas as atividades, é extraído um array de bytes da stream

Em seguida, é iniciada a construção da resposta, informando o código de status da mesma. Isto é feito através da execução do método setStatus, passando o código que significa sucesso no atendimento da solicitação. O HTTP define cinco classes de código de status que indicam, por exemplo, erro na solicitação do cliente, erro no processamento no servidor ou necessidade de redirecionar a solicitação para outra URL.  

Feito isto, é definido o valor para o cabeçalho Content-Length da resposta, através do método setContentLength. O valor passado é a quantidade de bytes no array montado. 

Então, é utilizado o método setContentType para preencher o cabeçalho Content-Type com o valor application/octet-stream, cujo significado foi previamente explicado. 

Finalmente, é obtida uma stream de saída através do método getOutputStream e nela é gravado o conteúdo do array de bytes. Ao fechar esta stream, a resposta é enviada ao cliente J2ME. 

Interpretando a resposta 

Voltando ao fonte exibido na listagem 3, o método getResponseCode da HttpConnection tem como valor de retorno o código de status do servidor, que indica se a requisição pôde ser atendida com sucesso. 

Em caso positivo, é criada uma instância de DataInputStream a partir da stream de saída obtida com o método openInputStream da HttpConnection. 

Conforme explicado anteriormente, como o conteúdo da DataInputStream foi montado a partir de um conjunto de bytes construídos com DataOutputStream, é fácil fazer a recuperação dos tipos primitivos (e Strings, também). 

Para tanto, é montado um laço que permanece em execução enquanto existirem dados na stream. A cada iteração, é recuperado da stream um valor do tipo int, um valor do tipo String e outro valor do tipo int. Estes três valores são utilizados para criar um objeto Task, que é incluído em uma lista. 

Ao final do laço, está disponível o conjunto de atividades a serem executadas pelo consultor em sua visita ao cliente. 

Para finalizar, é percorrida a lista de listeners registrados para este processo e, em cada um deles, é executado o método loadFinished, passando a lista de atividades como parâmetro. 

Com isto, a thread secundária é finalizada. 

Na thread principal, a execução do método loadFinished faz com que a lista de atividades seja persistida num RecordStore. Além disso, caso o usuário não tenha desistido de aguardar a atualização das atividades, uma tela com as informações recuperadas é exibida para ele. No exemplo, quando o usuário indica que deseja interromper a espera pela atualização dos dados, o valor da variável showNewTasks é passado para false

A listagem 5 mostra uma possível implementação para o método loadFinished na classe principal da aplicação. 

Listagem 5

 public void loadFinished(Vector list) {
   
save(list);
    if (showNewTasks) {
        showTask();
    }
    else {
        showNewTasks = true;
    }
} 

 Conclusões 

A escolha por HTTP para fazer a comunicação entre aplicações J2ME e sistemas de back-end é a mais óbvia por dois motivos. Primeiro, o suporte a este protocolo é exigido desde a especificação 1.0 do MIDP. Segundo, hoje o HTTP é praticamente onipresente nas aplicações corporativas. 

Este modelo pode tirar proveito de sistemas que utilizem o padrão MVC, onde a aplicação J2ME representa apenas outra view do sistema. 

Neste exemplo foi usado DataStream para formatar as informações retornadas pela aplicação servidora. Esta é uma opção simples e é a mais indicada quando ambas as partes são desenvolvidas utilizando Java. Se a aplicação servidora for construída com outra tecnologia, talvez seja o caso de criar um protocolo específico para esta formatação. Usar XML para formatar os dados, infelizmente, não é apropriado neste caso por aumentar a quantidade de dados trafegados e exigir um maior processamento. 

O código apresentado diz respeito a uma aplicação de exemplo. Para simplificar sua interpretação, nele não foi feito nenhum tratamento de erro. Uma aplicação real teria que estar preparada para lidar com várias situações adversas que podem ocorrer em sua execução. Esta afirmação é mais forte para o Servlet que atende as solicitações. Ele deve estar preparado para retornar códigos de status que indiquem problemas nos parâmetros recebidos e erros internos. 

Finalmente, no exemplo presente neste artigo os parâmetros passados ao servidor dizem respeito aos critérios de seleção de uma consulta. Entretanto, eles poderiam ser, perfeitamente, informações para serem atualizadas no destino.