images decorativa

Algo bastante relevante na grande maioria das aplicações é a persistência. Persistência em aplicações Web e Desktop é geralmente conseguida através do uso de Sistemas Gerenciadores de Bancos de Dados (DBMSs – Database Management Systems), que são responsáveis por gerenciar e garantir a integridade dos dados, onde esses dados ficam guardados em HDs com bastante espaço (muitos GBs ou alguns TBs). No mundo mobile, especialmente para celulares, as coisas não funcionam bem assim (embora hajam exceções). Não temos DBMSs propriamente ditos, mas outras formas de armazenamento bastante parecidas com sistemas de arquivos tradicionais, e os dados são persistidos em regiões não-voláteis dos dispositivos: HDs pequenos (alguns GBs) ou em memória Flash (cartões de memória).

Voltando nossas atenções para Java ME, a versão oficial do MIDP não oferece acesso direto ao sistema de arquivos de dispositivo, mas usa uma abordagem complementar: Record Stores. Diferentemente de uma arquivo tradicional, que pode guardar qualquer coisa, um Record Store guarda um grupo de itens que possuem estruturas parecidas, lembrando muito o conceito de tabelas e registros em um banco de dados relacional.

O MIDP suporta persistência de dados através do Record Management System (RMS).

RMS

A API RMS abstrai os detalhes de acesso e persistência dos dados, provendo uma maneira uniforme de criar, destruir e modificar esses dados de forma independente do dispositivo utilizado. A classe responsável por essa abstração é a classe RecordStore localizada no pacote javax.microedition.rms.

RecordStores

Um Record Store é uma coleção de registro em que os MIDlets tem acesso localmente, ou seja, no próprio dispositivo. Cada Record Store é identifica por uma String normal java: é case-sensitive (diferenciação de maiúsculas e minúsculas) e também é Unicode, com um tamanho variável entre 1 e 32 caracteres. O nome de Record Store deve ser único para um conjunto de MIDlets (MIDlet Suite). Record Stores podem ser compartilhados entre MIDlets que pertencem ao mesmo MIDlet Suite (mas isso não se aplica a MIDlets pertencentes a MIDlets Suites diferentes).

O RMS define as seguintes operações sobre um registro dentro de um Record Store:

  • Adicionar um registro
  • Deletar um registro
  • Atualizar um registro
  • Recuperar um registro
  • Navegar entre os registros (Enumerate)

Uma outra característica interessante da classe RecordStore é que ela garante atomicidade das transações single thread, mas não garante essa atomicidade em multithread.

Trabalhando com RecordStore

Abrindo e fechando Record Stores

Para criar ou abrir um um Record Store você utiliza o método estático:

RecordStore.openRecordStore(String nameOfRecordStore, boolean create);

No primeiro parâmetro você especifica o nome do Record Store e no segundo você informa se ele deve ser criado ou não. Esse método retorna o Record Store aberto/criado. Ele pode lançar as sequintes exceptions:

  • RecordStoreException: lançada em resposta a alguns erro durante a criação do Record Store
  • RecordStoreNotFoundException: lançada se o Record Store não for encontrado e a flag create estiver false
  • RecorStoreFullException: lançad se o Record Store estiver cheio.
  • IllegalArgumentExceptions: o método recebeu argumentos inválidos.

Após terminar de utilizar o Record Store você deve fechá-lo utlizando o método:

closeRecordStore()

Removendo um Record Store

Para deletar um Record Store você utiliza o método estático:

RecordStore.deleteRecordStore(String nameOfRecordStore)

Aqui não tem mistério, você simplesmente chama esse método passando o nome do Record Store a ser deletado. Esse método pode lançar as seguintes exceptions:

  • RecorStoreException: um exceção genérica indicando erros ao deletar um Record Store
  • RecordStoreNotFoundException: o Record Store não foi encontrado.

Ao apagar um Record Store você deve seguir as sequintes regras:

  • Um MIDlet só pode deletar um Record Store pertencente ao seu MIDlet Suite
  • O Record Store a ser deletado deve estar fechado. Caso isso seja falso uma RecordStoreException será lançada.

Obtendo informações sobre um Record Store

Você pode obter informações sobre um Record Store específico. Aqui estão alguns do métodos mais utilizados:

  • getName: retorna o nome de um Record Store
  • getLastModified: retorna o tempo decorrido desde a última modificação ocorrida no recor store. Esse tempo retorna o mesmo valor do método System.currentTimeMillis().
  • getVersion: retorna um inteiro que é modificado toda vez que um novo registro é inserido, deletado ou modificado no Record Store
  • getSize: retorna o número de bytes de um Record Store
  • getSizeAvailable: retorna a quantidade de bytes que ainda estão disponíveis para o Record Store no dispositivo. Esse valor não informa a quantidade correta de bytes disponíveis, pois não leva em consideração o tamanho dos metadados do Record Store em questão.

Registros (Records)

Um Record Store contem um ou mais registros. Cara registro é formado por um array de bytes (byte []) e possuem um valor inteiro que representa um identificados, como um espécie de ID, utilizado para diferenciar um registro de outro dentro de um Record Store. O identificador de um registro não faz parte da informação que ele representa e é atribuído ao registro assim que ele é criado. Identificadores obedecem as sequintes regras:

  • O identificador atribuído ao primeiro registro criado em um Record Store possui sempre o valor 1.
  • O identificador atribuído a um novo registro seque a regra n+1, onde n representa o valor do identificador do último registro adicionado no Record Store.
  • Identificadores nunca são reutilizados. Toda vez que um registro é deletado seu identificador é perdido.

Adicionando Registros

Um novo registro é criado através do método addRecord, que retorna o valor inteiro representado o valor do identificador do registro recém-criado:

public int addRecord(byte[] data, int offset, int size)

Para utilizar esse método com suas classes Java, você deve transformar a informação contidas nelas em um array de bytes. Uma maneira bem legal de fazer isso é utilizar a classe DataOutPutStream que irá os valores que você precisa de uma determinada classe e então utilizar a classe ByteArrayOutputStream para transformar essas informações em um array de bytes para você.

Uma maneira legal de entender como manipular registro é através de um pequeno exemplo. Irei pegar o exemplo emprestado do livro J2ME in a Nutshell. Nesse exemplo você tem um objeto que representa os pontos marcados por um jogador em um jogo e você quer salvar esses dados em um Record Store chamado Scores. A classe que você quer armazenar é parecida com essa:

public class ScoreRecord
{

	private String name;
	private int score;

	public ScoreRecord()
	{

	}

	public ScoreRecord(String name, int score)
	{
		super();
		this.name = name;
		this.score = score;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getScore() {
		return score;
	}

	public void setScore(int score) {
		this.score = score;
	}

}

Aqui está o trecho de código que mostra como você faria para armazenar os pontos de um jogador

// Create an object to be written
ScoreRecord record = new ScoreRecord( );
record.playerName = "TopNotch";
record.score = 12345678;

// Create the output streams
ByteArrayOutputStream baos = new ByteArrayOutputStream( );
DataOutputStream os = new DataOutputStream(baos);

// Write the values to be saved to the output streams
os.writeUTF(record.playerName);
os.writeInt(record.score);
os.close( );

// Get the byte array with the saved values
byte[] data = baos.toByteArray( );

// Write the record to the Record Store
int id = recordStore.addRecord(data, 0, data.length);

Recuperando registros

Para recuperar o registro de um Record Store basta fazer o caminho inverso do código anterior utilizando o método getRecord:

public byte[] getRecord(int recordId)

Ao recuperar um registro você utiliza as classes DataInputStream e ByteArrayInputStream, bastante parecido com o exemplo anterior, onde utilizamos as classes ByteArrayOutputStream e DataOutPutStream. Uma dica legal para não confundir essas classes: Output = gravar e Input = recuperar.

Agora suponha que gostaríamos de recuperar o nome e os pontos de um jogador armazenados no nosso Record Stores Scores. Para fazer isso utilizamos o código abaixo:

byte[] data = recordStore.getRecord(recordId);
DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
ScoreRecord record = new ScoreRecord( );
record.playerName = is.readUTF( );
record.score = is.readInt( );
is.close( );

Atualizando Registros

Para atualizar um registro você deve conhecer o seu identificador. Com o identificador em mão, você pode atualizar o registro sem medo através do método setRecord:

public void serRecord(int recordId, byte[] data, int offset, int size)

Esse método recebe 4 argumentos, onde os dois primeiros são os mais interessantes:

  • O identificador (ID) do registro que será modificado
  • Os novos dados do registro

Voltando ao nosso exemplos do jogador, suponha que ele vez mais 10 pontos e nós devemos atualizar seu registro. O seguinte código faz isso:

// Modify the score
record.score += 10;
ByteArrayOutputStream baos = new ByteArrayOutputStream( );
DataOutputStream os = new DataOutputStream(baos);
os.writeUTF(record.playerName);
os.writeInt(record.score);
os.close( );
byte[] data = baos.toByteArray( );
// Write the record to the Record Store, overwriting the existing record
recordStore.setRecord(recordId, data, 0, data.length);

Removendo um registro

Para remover um registro você simplesmente chama o método deleteRecord, passando o identificador do registro a ser deletado:

public void deleteRecord(int recordId)

Lembre-se que ao deletar um registro seu ID não é reciclado!

Record Enumerations

Os métodos que acabamos de mostrar supõem que você conhece o identificador de cada registro. O grande problema é que isso não é verdade na maioria das vezes. O que fazer então??

Para superar esse problema a classes RecordStore possui o método chamado enumeratedRecords que você pode usar para buscar eficientemente em um Record Store o registro que você precisa:

	public RecordEnumeration enumerateRecords(RecordFilter filter, RecordComparator comparator, boolean keepUpdated)

O argumento filter é responsável por selecionar quais registros irão ser incluídos na enumeração (falarei sobre ele já, já). O segundo argumento, comparator, é utilizado para comparar dois registros; ele é utilizado para ordenar os registros na enumeração. O último argumento, keepUpdate, quando true indica que a enumeração irá manter-se atualizada toda vez que alguma mudança acontecer no Record Store.

Um RecordEnumeration é uma interface que contém um conjunto de métodos que são utilizados para iterar sobre registros. Diferentemente de uma Enumeration Java tradicional permite que você avance ou recue, podendo mudar a direção a qualquer momento. Você consegue isso através dos métodos hasNextElement e nextRecordId para avançar; para voltar são utilizados os métodos hasPreviousElement e previousRecordId. Um pequeno exemplo de como avançar e recuar através da RecordEnumeration:

RecordEnumeration enum = recordStore.enumerateRecords(null, null, false);

// Traverse forwards
while (enum.hasNextElement( )) {
    int id = enum.nextRecordId( );
    // Do something with this record id (not shown)
}

// Traverse backwards
while (enum.hasPreviousElement( )) {
    int id = enum.previousRecordId( );
    // Do something with this record id (not shown)
}

Após obter o identificador de um registros, você geralmente gostaria de acessar o conteúdo desse registro. Você pode fazer isso utilizando uma combinação dos métodos mostrados acima com o método getRecord:

// Traverse forwards
while (enum.hasNextElement( )) {
    byte[] record = enum.nextRecord( );

    // Do something with this record (not shown)
}

// Traverse backwards
while (enum.hasPreviousElement( )) {
    byte[] record = enum.previousRecord( );

    // Do something with this record (not shown)
}

Uma outra diferença entre um Enumeration normal e uma RecordEnumeration é que a última possui o método reset que permite mover o cursor para o começo da iteração:

// Traverse forwards
while (enum.hasNextElement( )) {
    byte[] record = enum.nextRecord( );
    // Do something with this record (not shown)
}
enum.reset( );  // Reset to initial state
// Read all the records again
while (enum.hasNextElement( )) {
    byte[] record = enum.nextRecord( );
    // Do something with this record (not shown)
}

Trabalhando com RecordEnumerations atualizadas

Quando chamamos o método enumerateRecords e passamos o último argumento (boolean keepUpdate) como false, mudanças feitas no Record Store não serão refletidas na enumeration. Isso traz duas consequências:

  • Novos registros adicionados não estão disponíveis na enumeration.
  • Se um registro for deletado antes de seu identificador ser recuperado pela enumeration, uma InvalidRecordIDException será lançada quando seu registro for utilizado através dos métodos getRecord(enum.nextRecordId( )) ou enum.nextRecord.

Uma forma de prevenir isso é passar o último argumento como true no método enumerateRecords como mostrado abaixo:

RecordEnumeration enum = recordStore.enumerateRecords(null, null, true);

Agora toda vez que um novo registro for adicionado, deletado ou atualizado, essas mudanças serão refletidas imediatamente na enumeration. A única desvantagem dessa abordagem é um consumo de memória um pouco maior (você não deve se preocupar com isso na maioria das vezes).

Quando você terminar de usar a RecordEnumeration, você deve utilizar o método destroy:

public void destroy()

Esse método irá liberar os recursos utilizados pela enumeration.

Record Filteres e Comparators.

Caso você não queira iterar sobre todo os registros em um Record Store, você pode criar um objeto que implemente a interface RecordFilter. Esse objeto agora poderá filtrar os resultados obtidos por uma RecordEnumeration.

A interface RecordFilter exige que você implemente apenas um método:

public boolean matches(byte[] data)

Após criar um filtro e implementar o método matches, basta passá-lo como parâmetro do método enumerateRecord que foi mostrado anteriormente. Ao iterar sobre a Enumeration, ela irá retornar apenas os registros que passarem pelo filtro implementado no método matches.

Voltando ao nosso exemplo do jogo, suponha que agora queremos recuperar apenas os jogadores que marcaram mais de 10,000 pontos. Para isso criamos o seguinte RecordFilter:

RecordFilter filter = new RecordFilter( ) {
    public boolean matches(byte[] data) {
        try {
            DataInputStream is = new DataInputStream(
            new ByteArrayInputStream(data));
            is.readUTF( ); // Skip name
            int score = is.readInt( );
            // Match scores over 10000
            return score > 10000;
        } catch (IOException ex) {
            // Cannot read - no match
            return false;
        }
    }
};

Uma vez que o filtro foi criado, precisamos apenas passá-lo como o primeiro argumento do método enumerateRecors:

// Use the filter to get an enumeration that contains only
// a subset of the records in the Record Store
RecordEnumeration enum = store.enumerateRecords(filter, null, false);
// Print those players whose scores match the filter
while (enum.hasNextElement( )) {
    byte[] record = enum.nextRecord( );
    ByteArrayInputStream bais = new ByteArrayInputStream(record);
    DataInputStream is = new DataInputStream(bais);
    System.out.println("Name: <" + is.readUTF( ) + ">");
    System.out.println("Score: <" + is.readInt( ) + ">\n");
    is.close( );
}
enum.destroy( );

Para impor uma ordem os registros em uma RecordEnumeration você deve implementar a interface RecordComparator. Essa interface também só exige que você implemente o seguinte método:

public int compare(byte[] first, byte[] second)

Quando você passa um RecordComparator como parâmetro na construção de um RecordEnumeration, esse método é chamado toda vez que um par de registro é comparado. Como essa comparação é feita depende da estrutura do registro e dos critérios utilizados na ordenação da enumeration, ou seja, depende de você. Os seguintes valores podem ser retornados dependendo do resultado da comparação entre os registros:

  • RecordComparator.EQUIVALENT: os dois registros são iguais de acordo com os critério de ordenação
  • RecordComparator.PRECEDES: o primeiro registros deve vir primeiro que o segundo
  • RecordComparator.FOLLOWS: o segundo registro deve vir primeiro

Usando mais uma vez o Record Store do jogo, suponha que nós agora queremos que a enumeration esteja ordenada pela número de pontos de cada jogador em ordem decrescente (primeiro os jogadores que fizeram mais pontos até os que fizeram menos pontos). Aqui está um RecordComparator para essa ordenação:

// Sort an enumeration using a RecordComparator
RecordComparator comparator = new RecordComparator( ) {
    public int compare(byte[] first, byte[] second) {
        try {
            DataInputStream isFirst = new DataInputStream(
                        new ByteArrayInputStream(first));
            DataInputStream isSecond = new DataInputStream(
                        new ByteArrayInputStream(second));
            // Use descending order of scores.
            String firstName = isFirst.readUTF( );
            int firstScore = isFirst.readInt( );
            String secondName = isSecond.readUTF( );
            int secondScore = isSecond.readInt( );
	        if (firstScore != secondScore) {
             return firstScore > secondScore ?
                         RecordComparator.PRECEDES :
                         RecordComparator.FOLLOWS;
         }
         // When the scores are equal, sort based
         // on the player name.
         int comp = firstName.compareTo(secondName);
         if (comp == 0) {
             return RecordComparator.EQUIVALENT;
         } else if (comp < 0) {
             return RecordComparator.PRECEDES;
         } else {
             return RecordComparator.FOLLOWS;
         }
     } catch (IOException ex) {
         // Cannot read - claim that they match
         return RecordComparator.EQUIVALENT;
     }
   }
};

Algumas Limitações dos Record Stores

Record Stores são uma boa alternativa para contornar os problemas de portabilidade e recursos encontrados ao desenvolver aplicativos utilizando a plataforma Java ME. Porém eles não são perfeitos. 😥

O primeiro grande problema é que um dispositivo com uma certa quantidade de espaço disponível para persistência não dá nenhuma garantia que seus MIDlets podem usar muito desse espaço. Algumas chamadas de método podem não responder da forma esperada dependendo do dispositivo.

O segundo grande problema é o limite do tamanho de cada registro. Mais uma vez isso depende do dispositivo que será utilizado e algumas chamadas de métodos podem não retornar algo esperado.

Vendo essas limitações, tenha em mente que você deverá testar seus MIDlets em alguns dispositivos que sejam o alvo da sua aplicação. Mostrei uma maneira bacana de fazer isso no post anterior a esse: https://rvlaraujo.wordpress.com/2009/11/20/trabalhando-com-midlet-um-pouco-mais-reais/.

Espero que tenham entendido o importante conceito de persistência em dispositivos móveis na plataforma Java ME. Persistência é sempre importante em qualquer tipo de aplicação, então estudem mais um pouco esse conceito. Alguns links úteis:

http://java.sun.com/javame/reference/apis/jsr037/ – API javax.microedition.rms

http://developer.sonyericsson.com/getDocument.do?docId=66103 – site SonyEricsson

http://developer.motorola.com/docstools/articles/Optimizing_Applications_2_20061101.pdf/ – Optimizing a Java ME Application Part 2: RMS Sorting

http://developer.motorola.com/docstools/articles/RMS_Sharing_20060401.pdf/ – Sharing Record Stores in MIDlet Suites

http://developer.motorola.com/docstools/articles/RMS.pdf/ – Using RMS on Motorola Java-Enable Handsets

Referências:

J2ME in a Nutshell – Kim Topley

Wireless J2ME Plataform Programming – Vartan Piroumian

Beginning Java ME Platform – Ray Rischpater

Anúncios