MokaByte Numero 29  - Aprile 1999 
 

DBF in Java

di 
Matteo Baccan

Come gestire archivi dati nel  formato DB3, uno dei più famosi e diffusi nel passato

In collaborazione con Dev


Vediamo come sia possibile creare una classe di interfaccia verso file DBF, contenente una serie di funzionalità con la stessa sintassi dei comandi CLIPPER. Il tutto senza usare l’interfaccia JDBC, ma tramite un’interfaccia diretta e più performante.
Il problema nell’usare i driver ODBC per accedere ai file DBP, è che tali driver hanno infatti una logica di funzionamento SQL. Non essendo il DBF un tipo di dato nato per essere trattato in modo SQL, il modo migliore per prenderne i dati è quello di accedere direttamente alla sua struttura, creando una classe specifica.
Lo scopo di questo articolo è quello di illustrare come sia possibile creare una semplice classe JAVA in grado di scorrere un DBF, prenderne i campi, ed eventualmente aggiornarne il contenuto, il tutto senza usare un driver JDBC. 
Un’altra funzionalità della classe che andremo a costruire sarà poi quella di avere una sintassi stile CLIPPER/XBASE, per favorire un eventuale passaggio di sorgenti da programmi Clipper o da qualsiasi dialetto xbase, verso JAVA. Esistono ancora molte applicazioni, scritte in questi linguaggi, che vengono tuttora sviluppate e sopportate. Rivederle in linguaggio JAVA solitamente porta ad una riscrittura totale del codice. Avendo però una classe in grado di emulare quantomeno la sintassi di accesso ai dati, questo passaggio risulterà sicuramente molto più semplice.
 

Definizione della struttura
 

Prima di partire con la stesura del codice della nostra classe andremo ad analizzare il formato DBF, per capirne il corretto funzionamento e come sia possibile intervenire per la creazione di algoritmi generici di accesso ai dati.
La struttura dati DBF ha una caratteristica molto interessante: ha, al suo interno, un’intestazione (detta header) contenente le specifiche di tutti i campi dell’archivio stesso.
Prendiamo un DBF qualsiasi, e proviamo a leggerne i primi 32 caratteri, tramite un editor esadecimale o un qualsiasi strumento che permetta una visualizzazione "a byte". Nel nostro caso useremo art.dbf (che potrete trovare sul sito ftp di Infomedia, insieme al sorgente completo della classe che andremo a creare). Partendo così da art.dbf ecco il valore dei primi 32 byte di tale archivio:

 

83 62 0C 06 0B 00 00 00 42 03 E4 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Il primo byte che incontriamo è quello che ci permette di capire se si tratta di un DBF o di un altro tipo di file. Se infatti contiene il valore 03, nella maggior parte dei casi è un DBF. Esistono delle eccezioni a questa regola: il valore 83 è quella più comune, e indica che l’archivio è accompagnato da un file DBT, contenente i dati dei campi testuali.
Subito dopo questo valore ci sono tre byte che contengono anno, mese e giorno di ultima modifica. Da qui possiamo facilmente capire che, dato che il campo che contiene il secolo è di un solo byte, è stato usato uno stratagemma per memorizzare in modo corretto il valore dell’anno di ultima modifica. Lo stratagemma è dato dal fatto che, al valore contenuto, deve essere sommato 1900, per ottenerne il valore corretto. Nel nostro caso il valore, in notazione esadecimale, è 62, che corrisponde ad un 98 decimale. Tale valore, sommato a 1900, riporta esattamente 1998. A questo punto potrebbe nascere un piccolo dubbio: "Ma cosa succede se supero il valore 255?". I progettisti della struttura DBF avevano pensato a questo fatto, ma in maniera molto marginale (supponendo che questa struttura dati venisse abbandonata prima di quella data). Per verificare cosa potrebbe accadere ad una vostra applicazione con archivi DBF nel 1900 + 256, cioè nel 2156, provate a impostare questa data sul vostro PC e ad utilizzarla, apportando delle modifiche ai dati. Nella migliore delle ipotesi il programma andrà semplicemente ad impostare il valore 0 nel secondo byte, ma vi potrebbero anche essere dei casi in cui si vada a sporcare il byte precedente, ed in questo caso l’applicazione non sarebbe più in grado di riaprire l’archivio!
Subito dopo le informazioni a riguardo della data di modifica, vi sono quattro byte che indicano il numero di record dell’archivio, nel nostro caso 0B cioè 11. Questa informazione è molto importante. Infatti è utilizzata da alcuni programmi, come dbase, per sapere in che punto dell’archivio ci si deve posizionare per aggiungere un record o fino a dove arrivare in caso di eliminazione dei record cancellati. Chiaramente se l’informazione riporta un numero di record scorretto, si potrebbero avere delle perdite di dati.
I due byte successivi indicano la grandezza dell’header. Questo vuol dire che, essendo 65536 il valore massimo che 2 byte possono contenere ed essendoci uno scarto di 32, dato dai primi byte dell’archivio, il numero massimo di campi che un DBF può contenere è 2047. Questa informazione è importante per capire quale sarà la migliore struttura dati da utilizzare per gestire le caratteristiche dei campi.
I due byte successivi sono invece quelli che indicano quanto la grandezza del record contenuto nel DBF. Nel nostro caso questo valore è E4 00, cioè 224. Da qui in avanti, fino al byte 32, i vari programmi che hanno gestito il formato DBF hanno dato una loro libera interpretazione. Ecco perché sarebbe buona norma non fare affidamento ad informazioni presenti fuori da questi primi byte.
Successivamente a questa prima parte di header, iniziano una serie di blocchi, grandi a loro volta 32 byte. Tali blocchi contengono le informazioni riguardanti i singoli campi, il loro nome, la loro tipologia e grandezza.
Anche in questo caso vi sono interpretazioni diverse sull’utilizzo dei byte. Tutte le implementazioni sono però concordi nell’utilizzare i primi 11 caratteri per il nome, terminante con 0. Questo vuol dire che se vi fossero i primi tre caratteri pieni, il quarto col valore 0 e il quinto e successivi a loro volta riempiti con dei valori validi, il nome del campo sarebbe da interrompere alla posizione quattro. Dopo il nome vi è l’informazione sul tipo di campo. I valori standard, presenti in questo byte sono: C carattere, D data, N numerico, L logico, M memo. A questo punto vi sono altri 2 byte che indicano la lunghezza del campo.
Finiti i blocchi dei campi, iniziano ad essere elencati tutti i record, preceduti da un carattere che, se ‘*’, indica che il record in questione è da considerarsi cancellato.
A questo punto sappiamo tutto ciò che server per poter utilizzare la struttura di questi archivi e possiamo finalmente iniziare a gestire dei file DBF.
 

La classe

La classe che creeremo si chiamerà xbase, in quanto avrà lo scopo di poter leggere e scorrere tutte le informazioni contenute all’interno di un qualsiasi archivio xbase, cioè DBF.
I metodi che implementeremo saranno quelli di apertura e chiusura archivio, spostamento dal primo all’ultimo record, controllo record cancellato e di acquisizione dati da un particolare campo.
Il primo metodo che andremo ad implementare sarà quello di apertura, cioè use. La classe JAVA più comoda, per poter effettuare una gestione binaria di un file è RandomAccessFile. Pertanto, all’interno del metodo use creeremo un oggetto di tale classe nel seguente modo:

 

RandomAccessFile dbf = new RandomAccessFile("art.dbf","r");

I passi successivi saranno quelli di leggere il tipo di archivio e i tre byte di informazione della data di ultima modifica, tramite il metodo readUnsignedByte dell’oggetto dbf. Questi dati ci serviranno, per fare un controllo di validità del DBF e gestire un eventuale errore di apertura. Successivamente andremo a leggere il numero di record, la posizione di inizio dei dati e la grandezza dei singoli record. L’unico problema che avremo nel fare questo, sarà il fatto che, i valori contenuti in questi byte, non sono allineati secondo l’ordine discendente INTEL, ma con un ordine ascendente. Pertanto non è possibile utilizzare i metodi readInt() e readUnsignedShort() dell’oggetto dbf, ma saremo costretti a costruire due metodi che leggano nel corretto ordine questi byte. Vediamo pertanto come dovremo leggere l’informazione sul numero di record:
 

int ch4 = dbf.read();

int ch3 = dbf.read();

int ch2 = dbf.read();

int ch1 = dbf.read();

if ((ch1 | ch2 | ch3 | ch4) < 0)

throw new EOFException();

int val = ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));

e come invece dovremo leggere l’indirizzo di partenza dei dati e la lunghezza del singolo record:
 

int ch2 = dbf.read();

int ch1 = dbf.read();

if ((ch1 | ch2) < 0)

throw new EOFException();

int val = (ch1 << 8) + (ch2 << 0);

Se per curiosità volete vedere come JAVA gestisce readInt() e readUnsignedShort() vi consiglio di andare a leggere i sorgenti della classe RandomAccessFile. Noterete che il numero di byte letti è lo stesso, ma il calcolo del valore di ritorno è esattamente il contrario. Per questa ragione non è possibile usare il metodo standard di lettura dati.
Il passo successivo sarà ora quello di andare alla posizione 32 per leggere i blocchi contenenti i dati dei singoli campi. Per effettuare questa operazione utilizzeremo una classe contenitore delle informazioni riguardanti i singoli campi e creeremo un’array di oggetti di tale classe. Tale array dovrà così essere inizializzato in base al numero di campi scritto nel header.
Vediamo ora la struttura di tale classe:
 

class fieldHeader {

public String fieldName;

public char dataType;

public int displacement;

public int length;

public byte decimal;

}

I dati di questa classe saranno tutti valorizzati dalla lettura del blocco dati del singolo campo. L’unica informazione che però non è presente in tale blocco è il displacement. Tale informazione sarà infatti l’unica calcolata dinamicamente e ci servirà per sapere la posizione del campo all’interno del record (per diminuire il numero di calcoli da effettuare, una volta richiesto un particolare campo).
A questo punto il metodo use è pronto listato 1 (il sorgente del metodo use della classe xbase):

public void use( String fn, String rw ) throws IOException {

      // Apertura file

      dbf = new RandomAccessFile(fn,rw);

      // Informazioni dell'header

      dbfType=dbf.readUnsignedByte();

      dbfLastUpdateYear =(byte)dbf.read();

      dbfLastUpdateMonth=(byte)dbf.read();

      dbfLastUpdateDay  =(byte)dbf.read();

      dbfNumberRecs     =readBackwardsInt();

      dbfDataPosition   =readBackwardsUnsignedShort();

      dbfDataLength     =readBackwardsUnsignedShort();

      dbfNumberFields   =(dbfDataPosition-33)/32;

      dbf.seek(32);

      // Struttura campi

      byte fieldNameBuffer[] = new byte[11];

      int  locn=0;

      //Il primo campo e' il campo di deleted. Non ? in struttura, ma

      //esiste e se contiene '*' il record e' cancellato, se ' ' il record

      //e' calido

      dbfFields = new fieldHeader[dbfNumberFields+1];

      dbfFields[0] = new fieldHeader();

      dbfFields[0].fieldName="@DELETED@";

      dbfFields[0].dataType='C';

      dbfFields[0].displacement=0;

      locn+= (dbfFields[0].length=1);

      dbfFields[0].decimal=0;

      // Ciclo di lettura dei campi

      for (int i=1; i<=dbfNumberFields; i++) {

         dbfFields[i] = new fieldHeader();

         // Nome

         dbf.read(fieldNameBuffer);

         dbfFields[i].fieldName= new String(fieldNameBuffer);

         int posZero = dbfFields[i].fieldName.indexOf( 0 );

         dbfFields[i].fieldName = 

              dbfFields[i].fieldName.substring(0,posZero);

         // Tipo

         dbfFields[i].dataType=(char)dbf.read();

         // Lunghezza

         dbf.skipBytes(4);

         dbfFields[i].displacement=locn;

         locn+=(dbfFields[i].length=dbf.read());

         // Decimali

         if( dbfFields[i].dataType=='N' )

            dbfFields[i].decimal=(byte)dbf.read();

         else {

            dbfFields[i].decimal = 0;

            int len = (byte)dbf.read();

            dbfFields[i].length += len*256;

         }

         dbf.skipBytes(14);

      }

   }

La fase successiva è ora la gestione del movimento fra i record del DBF. Per implementare i metodi che si occuperanno di tale operazione creeremo una variabile in grado di passare dal valore 1 al record valore massimo contenuto nell’archivio. Tale informazione è scritta nell’intestazione del DBF, ed è pertanto facilmente reperibile. 

Vediamo ora in dettaglio tali metodi:

 

private int curRec=1;

public int recno() {

return curRec;

}

public void gotop() {

curRec=1;

}

public void gobottom() {

curRec=dbfNumberRecs;

}

public void skip() {curRec++;

if( curRec>dbfNumberRecs+1 )

curRec=dbfNumberRecs+1;

}

La variabile curRec è utilizzata per contenere il numero del record corrente, mentre la variabile dbfNumerRecs è quella precedentemente impostata, dal metodo use.
Un’organizzazione di questo tipo ha il vantaggio di avere una rapidissima velocità di spostamento all’interno dei dati, superiore anche a quella di alcuni prodotti specifici di accesso a DBF (infatti, in tal modo non vi sono accessi a disco in questa fase, ma tutte le operazioni sono effettuate in memoria).
Il passo successivo è quello di dare la possibilità alla nostra classe di accedere alle informazioni contenute nei campi dei singoli record. Per fare questo useremo le informazioni di record corrente e le informazioni contenute nell’array dei campi del DBF.
Prima di questo dovremo però spostare il puntatore dell’oggetto dbf al primo byte del record da leggere. Per sapere la posizione esatta alla quale muoverci basterà moltiplicare il numero di record al quale desideriamo andare, meno uno, per la lunghezza del record. A tutto questo andrà poi sommata la posizione di partenza dei dati. Sintetizzando la posizione di inizio del record è data da:
 

dbfDataPosition + ((recno-1)*dbfDataLength)

A questa posizione andrà poi sommata la posizione del campo che desideriamo controllare, cioè il suo displacement. Pertanto se volessimo prendere il terzo campo del dbf al record 33 dovremo semplicemente fare il seguente calcolo per saperne l’esatta posizione:
 

dbf.seek(dbfDataPosition+((33-1)*dbfDataLength)

                             +dbfFields[3].displacement);

Ora il puntatore al file DBF si troverà esattamente sul carattere di partenza del campo che ci interesserà controllare. Pertanto se il campo fosse di tipo carattere di 10 byte basterà effettuare questa semplice lettura per ottenerne il valore:
 

byte dataBuffer[] = new byte[10];

dbf.read(dataBuffer);

String valore = new String(dataBuffer);

A questo punto la nostra classe DBF contiene tutto ciò che ci server per poter aprire un DBF, spostarsi attraverso i record e leggerne i valori dei campi. Su questa classe potremo poi costruire tutti i metodi utili alla navigazione all’interno dei record, come ad esempio il metodo di lunghezza header o il metodo per leggere la data di ultima modifica.
Le uniche cose che non possiamo ancora fare sono le operazioni di ricerca record, seek, all’interno del DBF. Infatti per poter effettuare delle operazioni di questo genere è necessario utilizzare gli indici del file, che sono elementi esterni al file DBF, non descritti nell’intestazione del DBF stresso. Per chi volesse comunque approfondire questo aspetto ho inserito nella documentazione della classe anche un documento che descrive la modalità con la quale è costituito un indice NTX.
 

Conclusioni

Creare delle classi che supportino i DBF con la sintassi Clipper è un’operazione molto facile, se ci si limita a leggerne di dati. Infatti se si volesse andare più in dettaglio, introducendo la gestione degli indici e la gestione degli accessi concorrenti, il lavoro da effettuare sarebbe molto più lungo e complicato.
 

Bibliografia
 

[1] http://java.sun.com/products/jdk/1.2/ Sito di riferimento per il JDK 1.2 usato per l’interfaccia DBF
[2] http://developer.java.sun.com/ Sito di riferimento per gli sviluppatori JAVA
[3] http://www.gdsoft.com/swag/ffe/ File Formats Encyclopedia v2.0. Il formato DBF
 

Matteo Baccan è uno specialista di progettazione e sviluppo in C++. È coautore di dBsee 4 e dBsee++, ed autore di dBsee400. Attualmente si occupa dello studio di tecniche avanzate di programmazione in ambito Internet/Intranet tramite Lotus Notes, presso ISA Italian Software Agency, e può essere contattato tramite e-mail all’indirizzo baccan@isanet.it

 
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it