|
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
|