MokaByte 64 - Giugno 2002 
Il mondo java embedded
V parte: RMS, Record Management System, il sistema per la gestione e il salvataggio persistente dei dati in J2ME-MIDP
di
Marco Tomà
Continuiamo, in questo articolo, ad occuparci del profilo MIDP (Mobile Information Device Profile). Questo mese parliamo del sistema di gestione della persistenza dei dati

Spesso nella realizzazione di applicazioni reali si ha la necessità di salvare dati in maniera persistente cioè in modo non volatile. Telefoni cellulari, pager, palmari e dispositivi wireless in generale dispongono tipicamente di due tipi di memoria: una volatile (RAM) che svolge le stesse funzioni della RAM di un personal computer ed una non volatile (ROM, FlashROM, NOVRAM, EEPROM ecc.) che invece ricopre il ruolo di "hard disk allo stato solido". Talvolta alcuni dispositivi utilizzano come "area di salvataggio dei dati" una porzione della memoria RAM opportunamente alimentata con una batteria tampone che impedisce la perdita dei dati quando il dispositivo viene spento o la batteria primaria si scarica. Una tipica gestione della memoria nei dispositivi embedded è riportata in figura 1. Occorre aggiungere che, solitamente, questi apparati non dispongono di un vero e proprio hard disk (nel senso "rotante" del termine...) e, spesso, sono "governati" da sistemi operativi con file system molto semplici e "rudimentali".


Figura 1 - Esempio di organizzazione di memoria di un dispositivo wireless.

La persistenza dei dati è garantita nella tecnologia MIDP attraverso il concetto di "database" che per definizione è un insieme persistente e integrato di dati dinamici, ai quali è possibile accedere attraverso opportune operazioni che ne permettono la descrizione e la manipolazione.
Le risorse hardware e software in gioco, non sono però tali da permettere l'implementazione di sofisticati e potenti database, va da se, quindi, che il sistema per la gestione e per il salvataggio persistente dei dati nel MIDP debba, giocoforza, essere semplice e "spartano".
Il sistema adottato, denominato RMS (Record Management System) è un semplice database orientato al record che è, quindi, l'unità minima di informazione manipolabile. Qualunque operazione sui dati (scrittura, lettura, modifica e cancellazione) sarà effettuata sui singoli record.
L'insieme dei record forma un record store che può essere paragonato alla tabella di un database e che rappresenta qui "il più alto grado di aggregazione dei dati" (non esiste un oggetto predefinito che raggruppi più record store (tabelle)).

 

I record store
Quando un record store viene creato da una MIDlet risiede nella stessa directory (o comunque nella stessa area di memoria) della MIDlet Suite di cui fa parte la MIDlet stessa, quindi MIDlet appartenenti a suite diverse non possono accedere al medesimo record store. Poiché in una suite si possono creare più record store ognuno di questi dovrà avere un nome univoco all'interno della MIDlet Suite in modo da evitare possibili conflitti. Tale nome (case-sensitive) costituisce l'identificativo del record store e può assumere una lunghezza massima di 32 caratteri (Unicode).
L'implementazione java del record store è l'omonima classe RecordStore (del package javax.microedition.rms) che attraverso una set di metodi permette la gestione del database e la manipolazione dei dati in esso contenuti.
Per l'apertura e/o creazione di un record store occorre richiamare il metodo (statico) della classe RecordStore:

public static RecordStore openRecordStore( String recordStoreName, boolean createIfNecessary );

il valore booleano passato come parametro permette (se vale true) di creare il database nel caso questo ancora non esista (altrimenti il metodo può solo aprire un database esistente). Il valore ritornato è un oggetto di tipo RecordStore che rappresenta appunto il database sul quale effettuare le operazioni. L' apertura / creazione di un record store può generare alcune eccezioni, in particolare: una RecordStoreException per segnalare una generica "condizione anomala" derivante da un'operazione effettuata sul record store, RecordStoreFullException indica il riempimento del record store e l'impossibilità ad eseguire l'operazione, infine una RecordStoreNotFoundException ad indicare che il record store richiesto (recordStoreName) non esiste o comunque è inaccessibile.
Ovviamente, oltre alla creazione e all'apertura un record store sono permesse le operazioni di chiusura e di distruzione:

public void closeRecordStore();
public static RecordStore deleteRecordStore(String rsName);

il primo metodo consente la chiusura di un database precedentemente aperto con il metodo openRecordStore, il secondo permette l'eliminazione completa del record store indicato come parametro dalla memoria. Entrambi i metodi possono generare un'eccezione del tipo RecordStoreException, l'operazione di chiusura può inoltre comportare una RecordStoreNotOpenException se viene tentata la chiusura di un database mai aperto, mentre il tentativo di cancellare un record store inesistente lancia una RecordStoreNotFoundException.

Ogni record store può essere suddiviso, dal punto di vista logico in due parti: un header contenente informazioni sul record store stesso e sui dati in esso contenuti, e uno spazio riservato ai dati (data-blocks) contenente i record inseriti. Ogni data-block mantiene un puntatore al blocco successivo formando una sorta di lista linkata, l'header contiene un riferimento al primo blocco della lista e al primo blocco disponibile (che corrisponde a quello successivo all'ultimo record inserito).
Nell'header viene inoltre memorizzato il numero di record presenti nel record store (inizialmente tale valore è zero) incrementandolo e decrementandolo rispettivamente ad ogni aggiunta o cancellazione di un record. Un'altra importante informazione contenuta all'interno dell'header di un record store è il numero di versione. Il suo valore iniziale vale tipicamente zero, (ma dipende comunque dalle varie implementazioni del MIDP), tale valore viene poi incrementato ogniqualvolta viene eseguita un'operazione sul database (aggiunta, cancellazione, modifica di un record ) e può risultare molto utile ai MIDlet per verificare se un dato record store è stato modificato o meno da altri MIDlet (della stessa suite per ragioni di "visibilità"), da un altro processo o altro thread.
Per quanto riguarda la modifica, nell'header del record store è presente un campo "last modified time" (ritorna un valore con lo stesso formato di quello ritornato da System.getCurrentMilliseconds() ) che indica il momento in cui è avvenuta l'ultima modifica e viene aggiornato contemporaneamente al numero di versione. Come detto l'header tiene traccia del primo blocco libero (nextRecordId) disponibile per l'eventuale inserimento di un nuovo record, l'accesso a tale informazione può essere utile per poter determinare l'id del record prima ancora che questo venga effettivamente allocato. Questo può risultare particolarmente comodo nel caso si voglia realizzare una sorta di "database relazionale" legando tra loro più record store (che in questo caso "somiglierebbero più che mai a tabelle"), utilizzando il recordId per le relazioni tra i record di un record store e quelli salvati in un altro. A questo proposito, occorre aggiungere che l'implementazione del record store del MIDP non si occupa in alcun modo delle "transazioni" e ogni operazione sui record è atomica, quindi volendo realizzare un "database-relazionale" occorre tener conto di tali limitazioni inserendo opportuni lock. Se, ad esempio, si volesse mettere in relazione i record di un record store, chiamiamolo RS_1, con quelli di un secondo record store RS_2 utilizzando il nextRecordId, si potrebbe verificare il caso in cui un processo o un thread procede alla lettura del nextRecordId dello store RS_1 e al suo inserimento in un record da salvare nel RS_2 (tale valore rappresenta la relazione tra il record presente nel RS_1 e quello dello store RS_2). Se tra la lettura del nextRecordId di RS_1 da parte del processo o di un thread qualche altro processo o thread ha aggiunto un record a RS_1 nel record inserito in RS_2 ci sarà una relazione errata (un vecchio valore di nextRecordID). Nel realizzare strutture simili occorre tenere presente queste limitazioni e, provvedere alla corretta sincronizzazione tra i vari thread che operano sui record store in modo da garantire la consistenza dei dati.
Tutte le informazioni contenute nell'header sono accessibili tramite opportuni metodi:

public long getLastModified();
public int getNextRecordId();
public int getNumRecords();
public int getVersion();

Tutti i campi presenti nell'header di un record store, forniscono informazioni "interne" al record store stesso, permettono cioè di valutare la situazione interna di un record store in un determinato momento. Talvolta può essere utile ottenere alcune informazioni "esterne" al record store o meglio informazioni sul suo "contenitore". A tale proposito nella classe RecordStore sono presenti tre metodi:

public int getSizeAvailable();
public int getSize();
public String[] listRecordStores();

Il primo metodo ritorna il numero di byte ancora disponibili per una eventuale crescita del record store, il secondo getSize() fornisce il numero di byte occupati dal record store (in realtà il valore ritornato tiene conto non solo dello spazio effettivamente occupato dallo store ma anche di quello ad esso funzionale che ne consente il funzionamento e che varia in base alle implementazioni, ad esempio le strutture per la tenuto dello stato del record store).
L'ultimo fornisce invece l'elenco (i nomi) dei record store presenti in una MIDlet suite (quella da dove viene effettuata la chiamata, visto che i record store sono visibili solo all'interno della suite).

Si è può volte accennato al fatto che tutte le operazioni eseguite su un record store, vengono effettuate sui singoli record. Tali operazioni sono quelle classiche di un database: inserimento, modifica, lettura e cancellazione di un record. I metodi:

public int addRecord ( byte[] data, int offset, int byteNr );
public void deleteRecord ( int recordId );
public byte[] getRecord ( int recordId );
public int getRecord ( int recordId, byte[] buffer, int offset );
public void setRecord ( int recordId, byte[] data, int offset, int byteNr );

consentono appunto l'esecuzione di tali operazioni. Ogni record è individuato, univocamente, da un identificativo recordId (il primo record di un resord store ha id uguale a 1) che è il valore ritornato dal metodo addRecord e che occorre passare ai come parametro ai metodi deleteRecord, getRecord e setRecord. Un errato utilizzo di questi metodi o eventuali parametri non validi possono generare eccezioni: InvalidRecordIdException se l'id del record sul quale si tenta l'operazione non è valido (non esiste), RecordStoreFullException se l'operazione non può essere eseguita a causa del raggiungimento delle dimensioni massime consentite ed, infine, una RecordStoreNotOpenException se l'operazione è tentata su di un record store non ancora aperto (con il metodo openRecordStore).

 

Monitoraggio del record store: l'interfaccia RecordListener
Spesso può essere utile poter monitorare un record store, o meglio aver notizia di tutto ciò che accade al suo interno cioè eventuali aggiunte, modifiche, cancellazioni di record in modo automatico e trasparente. Tutto ciò è possibile implementando l'interfaccia RecordListener.
Questa infatti permette, tramite i tre suoi metodi:

public void recordAdded( RecordStore store, int recordId );
public void recordDeleted( RecordStore store, int recordId );
public void recordChanged( RecordStore store, int recordId );

di avere notifica rispettivamente dell'inserimento, della rimozione o del cambiamento di un record individuato da recordId e relativo al record store chiamato "store". Per poter rendere attivo "l'ascolto" sui cambiamenti riguardanti un dato record store, questo deve essere "collegato" ad un RecordListener (una classe che implementa l'interfaccia), con il metodo della classe RecordStore

public void addRecordListener ( RecordListener listener );

dopodiché ogni operazione sullo store genererà una chiamata ad uno dei tre metodi appena visti in base all'operazione effettuata ( aggiunta - cancellazione - modifica ). L'implementazione di questi tre metodi permetterà di ottenere il risultato voluto. Un semplice esempio riguardante una ipotetica rubrica telefonica può essere un sistema di notifica all'utente sulle operazioni svolte con messaggi del tipo:

  • Aggiunto nuovo numero: NOMINATIVO - NUMERO
  • Eliminato numero: NOMINATIVO - NUMERO
  • Modificato numero: NOMINATIVO - NUMERO.

Tutto ciò si può ottenere con semplicità ad esempio inserendo una Alert nei tre metodi e passandole come parametro il messaggio desiderato, la porzione di codice riportata sotto mostra questa situazione:

public class PhoneBook extends MIDlet implements CommandListener, RecordListener {

........
........
........

  
  // Richiamato a seguito della cancellazione di un
  // record dalla rubrica avverte dell'avvenuta eliminazione
  // della voce creando una Alert. Alla Alert vengono passati come   // parametri:l'indice del record eliminato, il nominativo e il   // numero di telefono corrispondenti.
  
public void recordDeleted(RecordStore rStore, int recordId){
    Alert alert = new Alert("Cancellata voce dalla rubrica",                             "Posizione: "+ recordId +
                            "\nNome: "+ phoneOwner +
                            "\nNumero: "+
                            phoneNumber,
                            deleteIcon, AlertType.INFO);

    alert.setTimeout(Alert.FOREVER);
    display.setCurrent( alert , form );
  }

   // Richiamato a seguito dell'aggiunta di un record
  // alla rubrica avverte dell'avvenuto inserimento della voce
  // all'indice recordId passato alla Alert. Nominativo e numero di
  // telefono * inseriti completano le informazioni visualizzate
  public void recordAdded(RecordStore rStore, int recordId){
    Alert alert = new Alert("Aggiunta nuova voce alla rubrica",                             "Posizione: "+ recordId +
                            "\nNome: "+ phoneOwner +
                            "\nNumero: "+ phoneNumber,
                             addedIcon,
                             AlertType.INFO );
    alert.setTimeout(Alert.FOREVER);
    display.setCurrent( alert , form );
  }

  // Richiamato a seguito della modifica di un record della rubrica
  // avverte dell'avvenuta modifica della voce individuata da
  // recordId. La Alert visualizza inoltre i dati modificati*/
  public void recordChanged(RecordStore rStore, int recordId){
    Alert alert = new Alert("Modificata voce della rubrica",                             "Posizione: "+ recordId +
                            "\nNome: "+ phoneOwner +
                            "\nNumero: "+ phoneNumber,
                            modifyIcon,
                            AlertType.INFO );
    alert.setTimeout(Alert.FOREVER);
    display.setCurrent( alert , form );
  }

  ...
  ...
}

O ancora si possono utilizzare tali metodi per aggiornare in modo semplice e "automatico" una lista di riepilogo (ad esempio la lista delle voci presenti nella rubrica ), il codice sotto è relativo a questa situazione:

public class PhoneBook extends MIDlet
  
                     implements CommandListener, RecordListener {

  private List phoneBookList;
  ...
  phoneBookList = new List(" RUBRICA TELEFONICA ", List.IMPLICIT);
  ...
  ...
  ...

  // Aggiorna la lista phoneBookList cancellando il record
  // eliminato dal record store, e individuato da recordId
  
public void recordDeleted(RecordStore rStore, int recordId){
    phoneBookList.delete(recordId);
  }

  // Aggiorna la lista phoneBookList inserendovi il nuovo
  // record aggiunto al record store
 
public void recordAdded(RecordStore rStore, int recordId){
    try {
      phoneBookList.append(new String(
                           rStore.getRecord(recordId)), null);
    }
    catch ( RecordStoreNotOpenException noEx ){}
    catch ( InvalidRecordIDException irEx ){}
    catch ( RecordStoreException rsEx ){}
  }

  // Aggiorna la lista phoneBookList modificando il record
  // modificato nel record store.  RecordId e l'indice degli
  // elementi della lista phoneBookList corrispondono, il primo
  // elemento della lista è il primo record del record store
  public void recordChanged(RecordStore rStore, int recordId){
    try {
      phoneBookList.set(recordId,
                        new String(rStore.getRecord(recordId)),                         null);
      }
      catch ( RecordStoreNotOpenException noEx ){}
      catch ( InvalidRecordIDException irEx ){}
      catch ( RecordStoreException rsEx ){}
  }

 

RecordEnumeration
Abbiamo visto che i record presenti in un record store, quindi per ottenere un metodo da uno store è sufficiente richiamare getRecord al quale passare l'id del record desiderato. Per ottenere l'elenco completo dei record presenti in uno store si può utilizzare un ciclo for su tutti gli elementi presenti nel modo seguente:

// Esegue un ciclo completo sui record presenti nel record
// store "store" partendo dal primo elemento che ha identificativo // 1
for(int i=1; i<= store.getNumRecords(); i++){
  byte[] record = store.getRecord(i);
}

A seguito di operazioni di cancellazione fatte sul record store (deleteRecord (int recordId), può accadere che alcuni id passati al metodo getRecord (i) nel ciclo for non corrispondano ad alcun record con un conseguente lancio dell'eccezione InvalidRecordIdException. Per ovviare a questo inconveniente è possibile in alternativa utilizzare l'interfaccia RecordEnumeration. Il ciclo for precedente può quindi essere sostituto dal codice seguente:

RecordEnumeration e = store.enumerateRecords(null,null,true);
while(e.hasNextElement()){
  byte[] record = e.nextRecord();
}

Il MIDP definisce RecordEnumeration come una interfaccia che dovrà però essere implementata dalle aziende produttrici di hardware/software compatibile con J2ME-MIDP in realtà quindi per il programmatore "finale" questa è a una classe e non una interfaccia.
Come è possibile vedere dal codice riportato sopra (il secondo ciclo for) per ottenere una enumerazione di record occorre richiamare il metodo della classe RecordStore :

RecordEnumeration enumerateRecord(RecordFilter filter,                                   RecordComparator comparator,
                                  boolean keepUpdated);

Degli oggetti RecordFilter e RecordComparator parleremo tra breve, l'ultimo parametro passato al metodo, keepUpdated, serve ad indicare se si desidera un continuo aggiornamento dell'enumerazione. Infatti settando keepUpdate a true, l'"enumeratore" terrà conto di tutte le modifiche apportate al record store, nel caso valga false invece dovrà essere compito del MIDlet effettuare un aggiornamento con l'utilizzo del metodo

public void rebuild();

E' possibile leggere e settare lo stato di keepUpdated con i metodi:

public boolean isKeptUpdated();
public void keepUpdated( boolean keepUpdated );


I metodi:

public boolean hasNextElement();
public boolean hasPreviousElement();

verificano la presenza o meno di un record "nelle due direzioni", mentre con i metodi:

public byte[] nextRecord();
public byte[] previousRecord();

public int nextRecordId();
public int previousRecordId();

è possibile ottenere il record precedente o quello successivo o i loro indici.
Un oggetto di tipo RecordEnumeration può essere resettato ( public void reset() ), in questo caso tutti gli indici dell'enumerazione vengono riportati allo stato in cui si trovavano quando l'"enumeratore" è stato creato, chiamando invece il metodo:

public void destroy();

vengono liberate tutte le risorse di cui il RecordEnumeration disponeva. Un oggetto di tipo RecordEnumeration sul quale è stato chiamato il metodo destroy non è più utilizzabile: qualunque riferimento dopo la sua distruzione genera una IllegalStateException.

 

Filtraggio, ordinamento e ricerca di record: RecordFilter e RecordComparator
Le interfacce RecordFilter e RecordComparator forniscono due semplici strumenti per il "filtraggio" e l'ordinamento dei record.
L'interfaccia RecordFilter possiede un solo metodo:

public boolean matches(byte[] candidateRecord);

Questo metodo viene richiamato dal RecordEnumeration, e implementa le regole di "filtraggio" (o meglio, le regole di "filtraggio" devono essere implementate dal programmatore in modo tale da ottenere solo i record che soddisfano le regole imposte dal "filtro"), in questo caso l'enumerazione conterrà solo i recod che hanno determinati requisiti.
Nell'esempio seguente il RecordEnumeration tornerà tutti e soli i record il cui primo carattere è la lettera "A":

public class Example extends MIDlet
                     implements CommandListener, RecordFilter {
  ...
  ...
  // L'enumerazione utilizza un filtro che è
  // implementato nel metodo matches di questa classe.
  // In pratica solo i record che iniziano con la lettera A saranno   // presenti nell'enumerazione
  RecordEnumeration e = store.enumerateRecords(this,null,true)

  while(e.hasNextElement()){
    byte[] record = e.nextRecord()));
  }
  ...
  ...
  // Trasforma il record in stringa e controlla l'iniziale.
  // Ritorna true se il record contiene una stringa che inizia
  // con la lettera A, false in caso contrario
  public boolean matches(byte[] recordCandidate ){
    String candidate = new String (recordCandidate);
    if (candidate.startsWith("A"))
      return true;
    else
      return false;
  }

L'interfaccia RecordComparator possiede anch'essa un solo metodo:

public int compare ( byte[] record_1, byte[] record_2);

che, come nel caso del RecordFilter, viene richiamato da RecordEnumeration. In questo l'enumerazione tiene conto del risultato dell'operazione di "confronto"( il criterio di confronto deve essere implementato dal programmatore). In particolare tale risultato può essere:

  • · EQUIVALENT: se, per il criterio di ricerca o di ordinamento, i due record sono uguali;
    · FOLLOWS: se il primo record (record_1), in base ai criteri di ricerca o di ordinamento, segue record_2;
    · PRECEDES: se il primo record (record_1), in base ai criteri di ricerca o di ordinamento, precede record_2;

Il semplice esempio seguente riporta un'implementazione del metodo compare(..) per il riordino dei record presenti in un record store in base alla loro lunghezza.

public class Example extends MIDlet
                     implements CommandListener, RecordComparator {
  ... 
  ...
  // L'enumerazione utilizza un comparatore che è implementato
  // nel metodo compare di questa classe. In pratica i record,
  // nell'enumerazione, vengono ritornati in ordine di lunghezza
  // dal * più grande al più piccolo
  RecordEnumeration e = store.enumerateRecords(this,null,true);
  while(e.hasNextElement()){
    byte[] record = e.nextRecord()));
  }

  // Controlla la lunghezza dei record e le riordina in base
  // alla lunghezza: la più lunga precede la più corta, se la   
  // lunghezza è la stessa il risultato è EQUIVALENT
  public int compare(byte[] record_1, byte[] record_2 ){
    if( record_1 > record_2)
      return RecordComparator.PRECEDES;
    else{
      if( record_1 < record_2 )
        return RecordComparator.FOLLOWS;
      else
    }
    return RecordComparator.EQUIVALENT;
}

 

Riferimenti
MokaByte: Il mondo java embedded
http://www.mokabyte.it/2002/01/j2me_1.htm

MokaByte: La configurazione CLDC
http://www.mokabyte.it/2002/02/j2me_2.htm

Mokabyte: Il profilo MIDP
http://www.mokabyte.it/2002/03/j2me_3.htm

Mokabyte: Grafica e gestione delle interfacce utente nel profilo MIDP
http://www.mokabyte.it/2002/05/j2me-4.htm

http://developer.java.sun.com/developer/technicalArticles/wireless
Developing Wireless Applications using J2ME[tm] Technology - Bill Day http://wireless.java.sun.com/getstart/articles/wirelessdev/wirelessdev.pdf
http://java.sun.com/products/midp/

Le specifiche del formato png versione 1.0 (W3C)
http://www.w3.org/TR/REC-png

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it