mercoledì 27 febbraio 2008

Netatalk su Ubuntu Linux per Mac OSX

Il nuovo OSX non accetta in alcun modo l'autenticazione su un server Netatalk usando password in chiaro. Se da un lato questo è un bene per la protezione degli utenti e delle loro credenziali, dall'altro è una seccatura per l'installazione di default di Ubuntu, che per motivi di licenza non prevede la suite di cifratura supportata dai Mac OSX. E' quindi necessario ricompilare ed installare a mano il pacchetto Netatalk con i seguenti comandi (Ubuntu 7.1):

# cd /usr/local/src/
# mkdir netatalk
# cd netatalk
# aptitude install devscripts cracklib2-dev dpkg-dev libssl-dev
# apt-get source netatalk
# apt-get source netatalk
# DEB_BUILD_OPTIONS=ssl sudo dpkg-buildpackage -us -uc
# debi

e la nuova versione di netatalk con il supporto per i nuovi Mac OSX è abilitato.

domenica 24 febbraio 2008

Boycott Trend Micro

I've signed the petition to boycott Trend Micro, that has sued Barracuda Networks for a patent right issue based on the use of the latter of the Open-Source GPL Clam AV antivirus software.

I'd like to express my support to Barracuda Networks, that is exploiting a GPL product accordingly to its licence, and so in a regular way. I also would like to applaud Barracuda for informing the community about its legal problems, a thing that probably Trend Micro didn't think could happen.
I cannot believe that someone could be sued due to patent rights on open source and GPL code, and this is an example of the need for a stronger protection against patents (like GPLv3 does), as well as it represents a nightmare for the open source community.
Moreover, I cannot really see the point in scanning a file travelling into a network, especially when this is done with an open source product. And if there is really a patent right, who will protect us againt viruses? Patent right holders? That's sound ridiculous to me!






giovedì 21 febbraio 2008

Dump di una query

Capita spesso di voler estrarre i dati da un database, tramite una query semplice o complessa, e di voler importare tali dati in un altro database. L'operazione può essere eseguita in modo piuttosto semplice dal client testuale psql.

La prima cosa da determinare è il tipo di query che fornisca come output i dati nell'ordine e con il layout (distribuzione delle colonne) desiderato. Una volta fatto questo, è sufficiente collegarsi al database sorgente, eseguire la query scrivendo i risultati su file, e far rileggere tale file dal database destinazione.

Sul database sorgente, tramite psql, occorrerà dare i seguenti comandi:

\a
\f ';'
\t
\o query.sql

ed eseguire quindi la query. I comandi impartiti a psql servono ad eliminare l'output formattato (\a), a impostare il separatore di colonna con un ';' (\f), a mostrare solo i dati senza intestazioni e numero totale di righe visualizzate (\t) e a scrivere automaticamente l'output della query sul file query.sql.

A questo punto è possibile collegarsi al database destinazione e ordinare la lettura e l'inserimento dei dati contenuti nel file query.sql. Per fare questo si può usare il comando \copy, ad esempio, supponendo di voler caricare i dati nella tabella myTable si può impartire il comando:

\copy myTable(col1,col2) from 'query.sql' with delimiter ';'

Si noti che viene specificato il file di origine (query.sql) e il delimitatore di colonna (;). Si noti inoltre come vengano specificate le colonne nelle quali i dati devono essere inseriti (col1, col2) in myTable. E' importante specificare le colonne ogni qual volta il dump dei dati non rispecchi esattamente il layout della tabella stessa, oppure se esistono colonne di tipo serial sulle quali non si vuole agire.

mercoledì 20 febbraio 2008

Linux on your desktop

I found this article where Kim Brebach lists 13 reasons why Linux should be your desktop system. The article is very interesting in my opinion, but it has some points that are not described in deep and, in my opinion, lead to wrong conclusions.

Let's start from point 9:
Keeping track of software -- Like most Windows users, I have a shelf full of software CDs and keep a little book with serial numbers under my bed in case I have to reinstall the lot. With Linux, there are no serial numbers or passwords to lose or worry about. Not a single one.
Well, this is almost wrong. First of all, it is not true that in Linux you don't have serial numbers, for instance, to install VmWare Server you have to require a a serial number. You can get it for free, but you must have it to install the product. Second, I believe that the statement above is, in general, a misconception of what Open Source is. Linux is the operating system, applications that run on top of it can be either free or not, can have a serial number or not, but this depends on the policy adopted by their owner. If such policy is compliant to Open Source, it is probably that you will not have to own a serial number. I believe that is more correct to say that it is GNU and Open-Source software that, in general, do not require to you serial numbers.

Reason 10 sounds quite strange to me:

Updating software -- Linux updates all the software on your system whenever updates are available online, including all applications programs. Microsoft does that for Windows software but you have to update each program you've added from other sources. That's about 60 on each of my PCs. More icing on the Linux cake is that it doesn't ask you to reboot after updates. XP nags you every ten minutes until you curse and reboot your machine. If you choose "custom install" to select only the updates you want, XP hounds you like a mangy neighborhood dog until you give in.

It is clear that Linux distributions have today very powerful update managers, and the one that I like the most is apt-get from Debian. But the packet manager usually take care only of the software it has installed, not the one the user has installed on her own. For example, I prefer to install Java related stuff (including IBM Eclipse) on my own, and thus I update them manually when I need it. The same happens in Microsoft Windows: every program manually installed must be managed manually, as well as every program automatically installed is managed by the update manager. The point here is that, thanks to Open-Source licences, Linux can have software repositories that include software not strictly related to the vendor. In other words, the lack is that Microsoft Windows does not have (and I don't believe can get it easily) a central repository that can provide users with non-Microsoft programs.
But please note also that the evolved packet managers are not an exclusive of Linux, since also other system have good software installation procedure (for instance the ports of a *BSD system).

Similarly, the point 11 states:

More security -- These days, operating systems are less vulnerable than the applications that run on them. Therefore a vital aspect of PC security is keeping your apps up-to-date with the latest security patches. That's hard manual labor in Windows, but with Linux it's automatic.
These seems a consequence of the previous point, but it does not consider that a package quality system must have time to check updates, dependencies and then produce new packages. This means that it can take a while before a security update for a specific application is available thru an automatic packaging system. Usually important updates are available soon, but this is not always the rule. Moreover, this could be done also with Microsoft Windows, even if often in commercial applications (even non Microsoft), security patches are lazily applied. Finally, for a manually compiled application, the update still is manually too, so there is no significant point.


In conclusion, I believe that the Brebach's article is very good, but it makes some generic assumptions on Linux that should be better applied to Open-Source. While it is true that today's Linux distributions include very automated package managers, this could be done also in other operating system, if the software licences and vendors allow it. I'm not stating here that the above three points are drawbacks, they are of course advantages, but I believe they are not explained correctly.

SVN: notifica via e-mail dei commit

Come già anticipato in un precedente articolo SVN consente di personalizzare le fasi pre e post commit mediante opportuni hooks. E' quindi possibile creare uno script che, nella fase successiva al commit (post-commit) effettui l'invio per e-mail delle informazioni di commit stesse.

Esistono diversi modi (e utility) per inviare per e-mail informazioni su un commit, ma uno semplice ed efficace è svnnotify (pacchetto ubuntu libsvn-notify-perl), uno script perl che consulta il repository SVN per reperire le informazioni da mandare via e-mail.

E' sufficiente creare uno script denominato post-commit nella directory hooks del repository SVN contenente i seguenti comandi:

EPOS="$1"
REV="$2"

# commands
SVNLOOK=`which svnlook`
SVNNOTIFY=`which svnnotify`
GREP=`which grep`

# from and to e-mail address, substitue with something appropriate
MAIL_TO="commit@myDomain.com"
MAIL_FROM=$MAIL_TO

# check projects to send e-mail from
MYREPO=javaRepo # the name of the repo, substitute with something appropriate
$SVNLOOK dirs-changed "$REPOS" | $GREP $MYREPO > /dev/null

# is this a project I'd like to send an-email for?
if [ $? -eq 0 ]
then
$SVNNOTIFY --repos-path "$REPOS" --revision "$REV" --svnlook "$SVNLOOK" --to "$MAIL_TO" --from "$MAIL_FROM" --attach-diff --subject-cx -i 50 -P "[commit]"
fi

Si noti che viene definito l'indirizzo di posta elettronica al quale inviare il resoconto, questo potrebbe essere una mailing list (tipicamente -commit) o un alias di posta per gli sviluppatori. Si noti inoltre che viene fatta una selezione sul repository per il quale inviare delle e-mail, tale controllo può essere omesso se si vuole ricevere notifica per ogni repository, o può essere esteso per includere altri repository.

Il comando svnnotify viene utilizzato cone le informazioni precedentemente definite, ed in particolare:
  • attach-diff che manda per allegato il diff relativo al nuovo commit;
  • subject-cx imposta il subject dell'e-mail in base al contesto (ad esempio viene visualizzato il percorso dei file modificati). Si noti l'utilizzo del flag -i per limitare la lunghezza della linea di soggetto;
  • -P per impostare un prefisso della linea di soggetto, così da facilitare il riconoscimento a colpo d'occhio (e mediante filtri automatici) del contenuto dell'e-mail stessa.

SVN: impedire commit senza commento

SVN presenta una serie di hooks per gestire le fasi di pre and post commit, cosa che risulta molto comoda ad esempio per verificare determinati requisiti prima del commit vero e proprio o fornire segnalazioni (ad es. e-mail) a commit avvenuto.

Per impedire che gli utenti inseriscano dei commit senza commento (o con commento troppo corto) è possibile inserire il seguente controllo nel file hooks/pre-commit:

REPOS="$1"
TXN="$2"

SVNLOOK=`which svnlook`

# check how many characters the user has placed in the comment
MESSAGE_TEXT=`$SVNLOOK log -t "$TXN" "$REPOS" `
COMMENT_LENGTH=`echo $MESSAGE_TEXT | grep "[a-zA-Z0-9]" | wc -c `
MIN_COMMENT_LENGHT_ALLOWED=12

if [ $COMMENT_LENGTH -le $MIN_COMMENT_LENGHT_ALLOWED ]
then
echo "*** ATTENZIONE ***" >&2
echo "Per ovvie ragioni non e' piu' possibile effettuare un commit con messaggio nullo o troppo corto." >&2
echo "Per favore, prenditi il tempo che occorre e scrivi un buon commento, servira' a comprendere meglio" >&2
echo "le modifiche che hai apportato." >&2
echo "Inoltre, se stai correggendo un errore/bug, includi alcune informazioni di test " >&2
echo "(ad esempio un lotto di test) che possano essere utili per controllare l'errore " >&2
echo "e per testare eventuali altre modifiche. " >&2
echo "" >&2
echo "Il testo da te inserito { $MESSAGE_TEXT } ha una lunghezza insufficiente: $COMMENT_LENGTH" >&2
echo "" >&2
exit 1
fi

Quello che avviene è piuttosto semplice:
  1. con svnlook si ottiene il messaggio di commit inserito dall'utente (si noti che il commit non è ancora stato confermato);
  2. si ottiene la lunghezza del messaggio di commit;
  3. se la lunghezza del messaggio è inferiore ad una lunghezza specificata, si provvede a notificare l'utente del problema e si termina in modo anormale lo script.
Si noti che l'avviso all'utente viene notificato attraverso lo standard error, e che lo script termina con un codice di errore non zero, ad indicare ad svn che il commit non puo' essere accettato. Per accettare il commit occorre infatti che lo script pre-commit termini con un codice di uscita pari a zero.

giovedì 14 febbraio 2008

Manifesto for Agile Software Development

A few days ago I signed the Manifesto for Agile Software Development in order to express my support to the Agile Software Development. This paradigm focuses more on what the client wants, rather than on the right technique to design and write software, leading to a development that is more face-to-face and day-by-day with the customer. In other words, instead of promoting a static and big software analysis, the Agile Software Development promotes a continue analysis on site, and a development with quick and small releases, that are directly tested. There are also other things the Agile Software Development promotes, but the key idea in my opinion is that you should not get too far from the customer when developing, and you should also be able to get and support day-by-day changes.

I signed the manifesto with the following motivation:

Agile Development represents to me agood way of thinking the development of a complex software. What I like the most is that changes are not scaring, but they are accepted as the development proceeds. Moreover, forcing small and frequent releases gives you and your client the feeling of having a product, even if still not complete. This is very important for the client, that can feel the development and can take control on it (meaning that she can collaborate with the developers in a "just-in-time" way), and it is very important for the developers too because they are forced to concentrate on incremental and significant changes, and they will not blow away a build.

domenica 10 febbraio 2008

Programmazione ad oggetti in C

La programmazione ad oggetti (OOP) è un potente paradigma utilizzato nella realizzazione di molti sistemi moderni. L'OOP è solitamente supportata a livello di linguaggio: il linguaggio mette a disposizione del programmatore una serie di astrazioni per realizzare i principi della OOP (incapsulamento, ereditarietà, polimorfismo). Ad esempio, in Java è disponibile il concetto di class, gli specificatori di accesso, l'estensione delle classi in ereditarietà (extends) e il polimorfismo sui metodi. In C++, similarmente, viene messo a disposizione il concetto di class, gli specificatori di accesso, l'ereditarietà (operatore : ) e il polimorfismo tramite le virtual table.

Programmare OOP con un linguaggio che supporta direttamente l'OOP è sicuramente molto comodo, ma questo non significa che solo con il supporto del linguaggio sia possibile programmare OOP. In questo articolo riassumo un'esempio didattico di programmazione OOP in linguaggio C, scritto da me come esercitazione per il corso di Fondamenti di Informatica C (prof. Cabri), Università di Modena e Reggio Emilia.

Considerazioni preliminari
Per ottenere un sistema OOP si deve riuscire ad implementare un tipo di dato astratto (classe), nascondere i dettagli implementativi di tale tipo di dato astratto (incapsulamento), rendere pubblici e accessibili determinati servizi (incapsulamento), consentire l'ereditarietà e rendere possibile il polimorfismo.
Il linguaggio C mette a disposizione le struct, che possono essere utilizzate per la definizione di un tipo di dato astratto (se dichiarate opportunamento con typedef). E' interessante notare come una struct possa contenere sia dati che puntatori a funzioni, e quindi possa contenere sia informazioni che servizi. Questo rende una struct molto simile ad una classe, anche se non consente di usare nessun specificatore di accesso (incapsulamento) e ereditarietà. E' comunque possibile utilizzare le struct per realizzare delle classi, e per farlo è necessario riuscire a nascondere i dettagli implmentativi privati (incapsulamento) e fornire supporto per ereditarietà. Si noti che il supporto per il polimorfismo è naturalmente garantito: siccome le struct possono contenere puntatori a funzioni, è sufficiente variare uno di tali puntatori per farlo puntare ad una funzione (e quindi ad un comportamento) differente.

Descrizione dell'esempio
L'esempio qui proposto è molto semplice: si vuole realizzare un tipo di dato corrispondente ad un conto corrente, quindi con saldo e servizi relativi all'aggiustamento di tale saldo (versamento, prelievo, stampa, riepilogo movimenti). Si consideri una definizione Java relativa ad una classe ContoCorrente:

public class ContoCorrente{
// dato incapsulato: saldo corrente
private float saldo = 0;

// dato incapsulato: array dei movimenti
private float[] movimenti;

// dato incapsulato: numero di conto
private int numeroConto = 0;

// costruttore
public ContoCorrente(int numeroConto);

// restituisce il saldo corrente
public int m_saldo();

// stampa i dati del contocorrente
public void m_stampa();

// versamento
public int m_versamento(int ammontare);

// prelievo
public int m_prelievo(int ammontare);

// stampa dei movimenti
public void m_stampa_movimenti();
}

Come si può notare, dall'esterno della classe non è visibile nessuna informazione circa l'implementazione del conto corrente (ossia come e dove sono memorizzate le informazioni dei movimenti, del saldo e del numero di conto). Gli unici servizi pubblici sono i metodi che consentono di agire sul conto corrente stesso.
Nel seguito si implementerà la classe Java di cui sopra in linguaggio C.

Implementazione tramite struct
E' necessario implementare due struct differenti, una pubblica e una privata. La struct pubblica deve contenere ogni membro pubblico della classe, nell'esempio di cui sopra solo funzioni (puntatori a funzioni). La struct privata conterra' invece i dettagli implementativi, ossia ogni membro non pubblico della classe di cui sopra. Ma come sono legate le due struct? Qui entra in gioco la potenza dei puntatori C: se le due strutture condividono lo stesso layout, allora è possibile effettuare un cast da una struttura all'altra. Ovviamente, essendo differenti, le due strutture non possono avere lo stesso layout, ma possono avere lo stesso layout delle parti sovrapposte.

Si consideri per prima la struttura pubblica, che risulta essere la seguente:

typedef struct CCPUB{
// puntatore alla funzione getSaldo
int (*m_saldo)(struct CCPUB* );

// puntatore alla funzione StampaContoCorrente
void (*m_stampa)(struct CCPUB*);

// puntatore alla funzione versamento
int (*m_versamento)(struct CCPUB*, int);

// puntatore alla funzione prelievo
int (*m_prelievo)(struct CCPUB*, int);

// puntatore alla funzione stampaMovimenti
void (*m_stampa_movimenti)(struct CCPUB*);
} ContoCorrentePub;

Come già detto sopra, la struttura pubblica contiene solo puntatori a funzione, essendo quelli gli unici membri pubblici specificati. La struttura privata deve includere anche i dati relativi a saldo, array dei movimenti, e numero di conto. Se queste informazioni vengono memorizzate in una struttura privata che abbia la stessa parte iniziale di quella pubblica, allora le due possono diventare compatibili. In altre parole, le strutture devono avere la stessa parte pubblica disposta nello stesso modo:

         - +--------------+ -
| | m_saldo | |
| +--------------+ |
| | m_stampa | |
Conto | +--------------+ | Conto
Corrente | | m_versamento | | Corrente
Pub | +--------------+ | Private
| | m_prelievo | |
| +--------------+ |
| |m_stampa_movim| |
- +--------------+ |
| NumeroConto | |
+--------------+ |
| Saldo | |
+--------------+ |
| Movimenti | |
+--------------+ |
| cur_mov | |
+--------------+ -

Avendo definito il layout delle strutture in questo modo, è possibile allocare la memoria necessaria a contenere tutte le informazioni (quindi la struttura privata) e rendere accessibile all'esterno solo un puntatore alla struttura pubblica. Così facendo, la struttura pubblica, o meglio il suo puntatore, impedisce l'accesso ai membri privati, mentre la memoria condivisa garantisce che le strutture siano sempre allineate. Ovviamente la conversione da struttura pubblica a privata non deve essere effettuata all'esterno, ma all'interno dei metodi pubblici del tipo di dato astratto. Infatti, sono solo questi che devono avere accesso alla struttura privata, e quindi come prima operazione di ogni metodo pubblico, occorre convertire il puntatore affinché punti alla propria struttura privata.
Si ipotizzi di aver memorizzato la dichiarazione (si tratta solo di una dichiarazione) della struttura ContoCorrentePub in un file CC_pub.h, che è l'unico header richiesto per poter usare il conto corrente. All'interno di un alto file CC_private.c si inserirà la dichiarazione della struttura completa dei membri privati:

typedef struct{
// puntatore alla funzione getSaldo
int (*m_saldo)(ContoCorrentePub*);

// puntatore alla funzione StampaContoCorrente
void (*m_stampa)(ContoCorrentePub*);

// puntatore alla funzione versamento
int (*m_versamento)(ContoCorrentePub*, int);

// puntatore alla funzione di prelievo
int (*m_prelievo)(ContoCorrentePub*, int);

// puntatore alla funzione di stampa dei movimenti
void (*m_stampa_movimenti)(ContoCorrentePub*);

//------------------------------------------

// dati nascosti
int NumeroConto; // numero del conto corrente
int Saldo; // saldo attuale
int Movimenti[MAX_MOVIMENTI]; // lista degli ultimi movimenti
int cur_mov; // indice del movimento corrente
} ContoCorrentePrivate;

All'interno dello stesso file si definiranno due metodi privati che consentono la conversione da un conto corrente pubblico ad uno privato:

/*
* Servizio privato di conversione dalla struttura pubblica a quella privata.
* Questa funzione accetta il puntatore alla struttura pubblica e lo converte in quello a
* struttura privata.
*/
static ContoCorrentePrivate* Public2Private(ContoCorrentePub* ccpub){


ContoCorrentePrivate* ccpriv = NULL; // puntatore da ritornare

// controllo se il puntatore è valido, e in caso punti realmente
// alla struttura pubblica lo converto (operazione di casting)
// nella struttura privata
if( ccpub != NULL ){
printf("\n\t[Public2Private] conversione della struttura da pub a private");
ccpriv = (ContoCorrentePrivate*) ccpub;
}

return ccpriv;
}



/*
* Servizio privato di conversione della struttura privata in quella pubblica.
* La funzione accetta il puntatore alla struttura privata e, se quest'ultimo è
* valido, lo converte in un puntatore alla struttura pubblica.
*/
static ContoCorrentePub* Private2Public(ContoCorrentePrivate* ccpriv){

ContoCorrentePub* ccpub = NULL; // puntatore da restituire

// controllo se il puntatore è valido, e in caso affermativo
// lo converto in un puntatore alla struttura pubblica
if( ccpriv != NULL ){
printf("\n\t[Private2Pub] conversione della struttura da private a pub");
ccpub = (ContoCorrentePub*) ccpriv;
}

return ccpub;
}

Come si può notare queste due funzioni sono piuttosto semplici: dichiarano il tipo di puntatore necessario e effettuano un semplice cast. Si noti che le funzioni sono dichiarate static, cosa che garantisce il fatto che esse siano private e confinate a CC_private.c.

A questo punto si consideri l'implementazione di uno dei metodi pubblici del conto corrente, per esempio quello relativo ad un versamento:

/*
* Servizio pubblico di versamento.
* Questo servizio accetta come parametro il puntatore alla struttura pubblica, provvede
* alla sua conversione a privata ed esegue il versamento.
* La funzione ritorna l'effettivo ammontare versato.
*/
int versamento(ContoCorrentePub* ccpub, int ammontare){
ContoCorrentePrivate* ccpriv;


// controllo che il puntatore sia valido e che l'ammontare sia positivo
if( ccpub == NULL || ammontare <= 0 ){ return 0; } // effettuo il versamento ccpriv = Public2Private( ccpub ); ccpriv->Saldo += ammontare;
registraMovimento( ccpriv, ammontare );
return ammontare;
}
Come si nota, il metodo effettua la conversione dalla struttura pubblica a quella privata, dopodiché lavora sui membri privati (es. Saldo). Analogamente, le altre funzioni si comporteranno in modo simile: effettueranno la conversione e lavoreranno sulla parte privata della struttura.

Creazione di un oggetto Conto Corrente
Resta da considerare il costruttore di questo tipo di dato astratto. Il costruttore deve allocare la memoria necessaria, nonché agganciare i puntatori della struttura pubblica alle relative funzioni definite nel file privato CC_private.c:

/*
* Servizio pubblico di generazione di un nuovo conto corrente. Questa
* funzione accetta il numero del nuovo corrente da aprire e crea/inizializza
* una struttura dati privata (ContoCorrentePrivate), per poi restituire il
* puntatore alla struttura pubblica (ContoCorrentePub).
*/
ContoCorrentePub* aperturaContoCorrente(int numero_conto){

ContoCorrentePrivate* ccpriv = NULL;

// alloco memoria per il nuovo conto corrente
ccpriv = (ContoCorrentePrivate*) malloc( sizeof(ContoCorrentePrivate) );

// controllo se la malloc ha allocato effettivamente memoria
if( ccpriv != NULL ){
// il nuovo conto corrente è stato generato, lo inizializzo
printf("\n\t[aperturaContoCorrente] inizializzazione dati conto corrente");
ccpriv->NumeroConto = numero_conto;
ccpriv->Saldo = 0;
ccpriv->cur_mov = 0;

// "aggancio" i puntatori alle relative funzioni
ccpriv->m_saldo = getSaldo;
ccpriv->m_stampa = StampaContoCorrente;
ccpriv->m_versamento = versamento;
ccpriv->m_prelievo = prelievo;
ccpriv->m_stampa_movimenti = stampaMovimenti;

}

// restituisco il conto corrente creato (o NULL se non si è riusciti
// ad allocare memoria
return Private2Public(ccpriv);
}


Come si può notare, viene allocata la memoria necessaria ad una struttura privata, dopodiché si agganciano i puntatori a funzione e si restituisce il puntatore ristretto alla struttura pubblica.

Utilizzo del tipo di dato astratto
Il seguente programma mostra l'utilizzo del conto corrente:

#include 
#include "CC_pub.h" // include che definisce la versione pubblica
// della struttura ContoCorrente


int main(void){
ContoCorrentePub* C1;
ContoCorrentePub* C2;



printf("\nProgramma in esecuzione\n");

// creo i conti corrente
C1 = aperturaContoCorrente( 3663 );
C2 = aperturaContoCorrente( 8026 );


// effettuo dei versamenti
C1->m_versamento( C1, 10000 );
C2->m_versamento( C2, 7000 );

// effettuo un prelievo ed un versamento combinato
C1->m_versamento( C1, C2->m_prelievo(C2, 2000) );

// richiedo la stampa delle informazioni dei conti
C1->m_stampa(C1);
C2->m_stampa(C2);

// stampa dei movimenti
C1->m_stampa_movimenti( C1 );
C1->m_stampa_movimenti( C2 );


}


Si noti che:
  • la struttura privata non è mai visibile, poiché l'apertura di un conto corrente avviene fornendo sempre un puntatore alla struttura pubblica. Se si tenta di accedere ad un campo della struttura pubblica si ottiene un errore di compilazione, poiché il campo non è presente nella struttura pubblica. Se si cerca di convertire manualmente la struttura da pubblica a privata, si ottiene un errore di compilazione (ContoCorrentePrivate non è definito in nessuno degli include del programma).
  • ogni chiamata a metodo deve specificare come argomento la struttura alla quale si riferisce.
L'ultimo punto potrebbe risultare oscuro, ma segue la prassi della programmazione OOP. Infatti, quando ad esempio in Java si effettua una chiamata

C1.m_versamento(200);

il compilatore trasforma la chiamata silenziosamente in

C1.m_versamento(C1, 200);
ciò è necessario per il funzionamento di this. In sostanza, ogni metodo di istanza viene modificato dal compilatore per includere come primo parametro l'istanza stessa sulla quale operare. Tuttavia, mentre in un linguaggio che supporta OOP questo avviene trasparentemente, in una simile soluzione è necessario specificarlo esplicitamente. Questo porta anche alla possibilità di errori quali:

 C1->m_versamento( C2, 10000 );
ove si pensa di agire su C1 ma per errore si agisce su C2. E' evidente che errori del genere sono dovuti alla assenza di supporto OOP nel linguaggio, ma come si è visto da questo esempio, è comunque possibile implementare sistemi OOP.

Ereditarietà
L'ereditarietà viene implementata in modo simile a quello visto per la conversione da struttura pubblica a privata: è possibile estendere una classe aggiungendo nuovi membri in fondo alla struttura, lasciando inalterate le parti sovrapposte alla superclasse. Ad esempio:

typedef struct{
// puntatore alla funzione getSaldo
int (*m_saldo)(ContoCorrentePub*);

// puntatore alla funzione StampaContoCorrente
void (*m_stampa)(ContoCorrentePub*);

// puntatore alla funzione versamento
int (*m_versamento)(ContoCorrentePub*, int);

// puntatore alla funzione di prelievo
int (*m_prelievo)(ContoCorrentePub*, int);

// puntatore alla funzione di stampa dei movimenti
void (*m_stampa_movimenti)(ContoCorrentePub*);

//------------------------------------------

// dati nascosti
int NumeroConto; // numero del conto corrente
int Saldo; // saldo attuale
int Movimenti[MAX_MOVIMENTI]; // lista degli ultimi movimenti
int cur_mov; // indice del movimento corrente

// variabili aggiunte nella sottoclasse
char* intestatario; // nome dell'intestario del conto corrente
int numeroCartaCredito; // numero di carta di credito
} ContoCorrentePrivateExt;
In questo caso, si aggiungono altri dati (ma potevano essere aggiunte altre funzioni) al conto corrente. Si noti che, essendo rimaste identiche le parti sovrapposte, il ContoCorrentePrivateExt è compatibile con ContoCorrentePrivate e quindi ne è una classe derivata.

Si noti che nell'esempio di cui sopra, l'ereditarietà non mostra tutti i vantaggi: è necessario ricopiare tutti i campi della struttura base. Si può ovviare a questo inserendo come primo campo della struttura la struttura base stessa:

typedef struct{
ContoCorrentePrivate* superclasse; // puntatore alla classe base

// variabili aggiunte nella sottoclasse
char* intestatario; // nome dell'intestario del conto corrente
int numeroCartaCredito; // numero di carta di credito
} ContoCorrentePrivateExt;


Polimorfismo
Ottenere il polimorfismo è abbastanza semplice: basta definire nuovi costruttori (metodi aperturaContoCorrente()) che aggancino i puntatori di funzioni a nuove implementazioni.

Considerazioni
Come si è visto è possibile implementare sistemi OOP anche in linguaggi non OOP, chiaramente con qualche difficoltà e possibilità di errori. Diversi sistemi OOP sono realizzati con linguaggi non OOP per diverse ragioni, le principali sono:
  • assenza di compilatori OOP;
  • maggior controllo sul tipo run-time rispetto a quello fornito dal compilatore;
  • adattamento ed evoluzione di un sistema originariamente non OOP.
Alcune implementazioni, come GTK+, mantengono due strutture separate: quella di classe e quella di istanza. Questo simula meglio ciò che avviene ad esempio in Java, ove una sola classe viene caricata (con la definizione dei metodi) e la memoria allocata per le istanze contiene solo i dati di ciascun oggetto.

Ridirezione dei comandi shell

Una cosa molto utile nelle shell Unix è la ridirezione dei comandi, ovvero la capacità di inviare l'output o reperire l'input di un comando da un file. La ridirezione viene effettuata mediante i caratteri speciali > e <. Ho notato che molto spesso gli studenti universitari tendono a confondere i tipi di riderezione, generando errori goffi nei loro programmi. In questo articolo vengono mostrate alcune semplici regole da tenere presente nell'utilizzo della ridirezione.


Ridirezione dello standard output

La ridirezione dello standard output avviene sempre con il carattere >, che deve essere seguito sempre da un file, e mai da un comando. E' possibile ridirezionare anche lo standard error, utilizzando la forma 2>. In generale, > supporta la ridirezione di ogni file descripto aperto dal processo. Ricordando che il file descriptor 1 è associato allo standard output, e il 2 allo standard error, si capisce come 2> ridirezioni appunto lo standard error. Per la stessa ragione, > e 1> svolgono la stessa funzione.

E' poi possibile ridirezionare i flussi di output specificando direttamente il file descriptor al quale fare riferimento. Ad esempio, se si ridireziona lo standard output, è possibile dire al sistema di ridirezionare lo standard error nello stesso modo. Questo si ottiene con 2>&1, che specifica che il file descriptor 2 (stderr) deve essere ridirezionato (>) come (&) il file descriptor 1. Una prassi comune è quella di avere:

comando > output.txt 2>&1

che cattura stdout e stderr e li salva assieme nel file output.txt.
Per concludere, ecco alcuni esempi:

Ecco alcuni esempi:
ls > elenco_file.txt
(memorizza la lista dei file in elenco_file.txt)
ls 1> elenco_file.txt
(come sopra)
ls > elenco_file.txt 2>errori.txt
(memorizza la lista dei file in elenco_file.txt, e gli errori, come ad esempio mancati permessi, in errori.txt)
ls > elenco_file.txt 2>&1
(memorizza la lista dei file in elenco_file.txt, assieme agli errori)
ls > elenco_file.txt 2>/dev/tty
(memorizza la lista dei file in elenco_file.txt e mostra a terminale gli errori)

ls > elenco_file.txt 2>/dev/null
(memorizza la lista dei file in elenco_file.txt e butta via gli errori)


La ridirezione in append segue le stesse regole, eccetto che utilizza la forma >> e che non prevede una forma complementare per lo stderr (non esiste 2>>&1). Si tenga comunque presente che un comando del tipo:

ls >> elenco_file.txt 2>&1

inserisce in append sia stdout che stderr.

Ridirezione dello standard input
La ridirezione dello standard input avviene sempre con il carattere <, che deve essere sempre preceduto da un comando e mai da un file. Non ha in questo caso senso specificare un numero di file descriptor, essendo lo stdin associato al file descriptor 0. Si tenga presente che, indipendentemente dal carattere di ridirezione usato, a destra di tale carattere si deve avere sempre un file, mentre a sinistra un comando. Ovvero:

comando > file
comando < file

Ridirezione fra processi
Qualora sia necessario passare lo stdout di un programma ad un altro programma, occorre utilizzare la pipe (|), che deve sempre avere alla sua sinistra e alla sua destra due comandi, mai dei file. E' quindi sbagliato scrivere qualche cosa come:

ls > wc -l
wc -l <>

mentre la forma corretta è:

ls | wc -l


Clobbing
La normale ridirezione dello stdout (>) ha un comportamento di default abbastanza pericoloso: se il file non esiste viene creato, ma se il file esiste il suo contenuto viene totalmente sovrascritto. In molti casi questo è un comportamento utile, ma rischia di far perdere il contenuto di un file per errori di battitura o di fretta. E' possibile disabiltare questo comportamento impostando il clobbing della shell. In sostanza, attivando l'opzione noclobber, la shell non consente la sovrascrittura di un file esistente (è invece consentita la ridirezione in append >>). Per settare l'opzione è sufficiente dare il comando seguente:

set -o noclobber 


e se si tenta di sovrascrivere un file esistente, la shell avverte l'utente con un errore:

echo "ciao" > elenco_file.txt

bash: elenco_file.txt: cannot overwrite existing file

E' comunque possibile evitare l'opzione di clobbing utilizzando lo speciale operatore >!, che consente di sovrascrivere il contenuto di un file sempre:

echo "ciao" >! elenco_file.txt
Si presti attenzione al fatto che il clobbing disabilita anche le funzioni di interpretazioni dei caratteri speciali della shell, ad esempio l'uso di * non funzionerà più.


Riassumendo quindi, qualora si effettui una ridirezione dello stdout, è opportuno procedere come segue:
  • se il file destinazione deve sempre essere azzerato, è bene usare l'operatore >!, così da evitare errori dovuti ad una errata impostazione del clobbing;
  • se il file destinazione potrebbe non essere sovrascritto (a seconda della volontà dell'utente), si utilizzi l'operatore di ridirezione normale (>).

Variabili di ambiente esportate con export

Molto spesso gli studenti dei corsi di Sistemi Operativi dell'Università degli Studi di Modena e Reggio Emilia mi hanno chiesto chiarimenti circa il funzionamento di export per l'esportazione delle variabili.

Il comando export consente di esportare una variabile definita in un processo shell per i sottoshell che saranno generati da essi. L'esportazione è simile alla semantica della fork(2) quando le variabili sono copiate da un processo padre a quello figlio. Si noti che la copia/esportazione si ha per valore in avanti, ossia dal padre al figlio, e non dai processi figli verso quello padre. In altre parole, al momento dell'export viene copiata la variabile nello shell figlio, ma i due valori restano indipendenti. Questo significa che se lo shell figlio modifica il valore, il padre non vede la modifca. E' facile verificare questo con una coppia di script:

#!/bin/bash
# script1.sh (script padre)

VAR1="variabile1"
echo "Script1 : $VAR1"
export VAR1
./script2.sh
echo "Script1 : $VAR1"


#!/bin/bash
# script2.sh (script figlio)
echo "Script2 : $VAR1"
VAR1="Variabile2"
echo "Script2 : $VAR1"


Come si può notare lo script1.sh definisce una variabile, la esporta, invoca un processo figlio e controlla nuovamente il valore della variabile. L'esecuzione di tale script produce il seguente output:

Script1 : variabile1
Script2 : variabile1
Script2 : Variabile2
Script1 : variabile1

Come si può notare, lo script1.sh vede sempre e solo il valore originale della variabile VAR1, anche se quest'ultima è stata modificata dallo script2.sh. Questo è dovuto proprio al fatto che l'export funziona dal padre al figlio e non viceversa.

Backup automatici di un database con pg_dump e crontab

PostgreSQL mette a disposizione uno strumento molto utile, pg_dump, che consente di effettuare un dump di un database (o di porzioni di esso) su un file di testo. Il file prodotto contiene i comandi SQL per la creazione e ripopolazione del database. E' quindi possibile utilizzare pg_dump per il backup dei propri database, magari utilizzando assieme ad una schedulazione automatica quale quella di crontab.

Nel seguito viene presentato un semplice script shell che, basandosi su pg_dump, effettua il backup di un database specifico. In particolare, lo script può funzionare in tre modalità:
  • single (default): effettua il backup del database e ne salva il dump in una directory con nome pari al giorno di backup (es. mer, gio, ven,...). Utile per fare il backup giornaliero del database, con rotazione settimanale.
  • dated: effettua il backup del database salvando il dump in un file isolato che include le informazioni relative alla data di backup. Utile per effettuare un checkpoint del database ad una specifica data; il backup non viene mai sovrascritto.
  • isolated: simile a single, ma memorizza i dump in una directory specifica per ogni database.
In sostanza, supponendo di dover fare il backup del database myDB, e immaginando di voler memorizzare i backup nella directory /backup/postgresql si ottiene il seguente layoyut per la modalità single:

+ /backup
+ postgresql/
+ lun/
myDB.sql
+ mar/
myDB.sql
+ mer/
myDB.sql
+ gio/
myDB.sql
+ ven/
myDB.sql
+ sab/
myDB.sql
+ dom/
myDB.sql


Si noti che ogni backup è contenuto in una cartella relativa al giorno della settimana nel quale è stato effettuato il backup. Nel caso della modalità dated si ottiene invece:

+ /backup
+ postgresql/
+ myDB/
myDB_01_01_08.sql
myDB_05_01_08.sql

Ossia si isola una directory con nome uguale a quello del database che si sta backuppando, e al suo interno si memorizzano file diversi secondo la data di backup (si noti che la data viene memorizzata nel nome del file). Infine, per il caso isolated si ha che:

+ /backup
+ postgresql/
+ myDB/
+ lun/
myDB.sql
+ mar/
myDB.sql
+ mer/
myDB.sql
+ gio/
myDB.sql
+ ven/
myDB.sql
+ sab/
myDB.sql
+ dom/
myDB.sql

ossia simile a quella single, dove però i giorni della settimana sono memorizzati all'interno di una directory specifica per il database.


Lo script è il seguente:

#!/bin/bash

DATABASE_NAME=$1
USERNAME=$2
HOST=$3
WORKING_DIR=$4
RUNNING_MODE=$5
PG_DUMP=/usr/lib/postgresql/8.2/bin/pg_dump


if [ "$RUNNING_MODE" = "--dated" ]
then
DATA=`date +'%h_%d_%y'`
WORKING_DIR="${WORKING_DIR}"/"${DATABASE_NAME}"
OUTFILE="${DATABASE_NAME}_${DATA}.sql"
else
DATA=`date +'%a'`

if [ "$RUNNING_MODE" = "--isolated" ]
then
WORKING_DIR="${WORKING_DIR}"/"${DATABASE_NAME}/${DATA}"
OUTFILE="${DATABASE_NAME}.sql"

else
# data = giorno della settimana
WORKING_DIR="${WORKING_DIR}"/"${DATA}"
OUTFILE="${DATABASE_NAME}.sql"
fi

fi


# Creo la directory se non esiste
if ! test -d "$WORKING_DIR"
then
mkdir "$WORKING_DIR" > /dev/null 2>&1
if [ $? -ne 0 ]
then
echo "Errore nella creazione della directory $WORKING_DIR"
exit
fi
fi



# entro nella directory di lavoro
cd "$WORKING_DIR"
START_TIME=`date` # istante di inizio


# esecuzione del backup effettivo
$PG_DUMP --create --column-inserts -f $OUTFILE --encoding=Latin1 -U $USERNAME -h $HOST $DATABASE_NAME
END_TIME=`date`

Come si può notare lo script prepara anzitutto la directory che deve contenere i file di dump, a seconda della modalità con la quale lo script viene eseguito. Successivamente ci si sposta nella directory relativa e si effettua il backup mediante pg_dump. L'opzione --column-inserts ordina a pg_dump di inserire nel file di dump dei comandi INSERT per la popolazione delle tabelle, al posto del default COPY. Si noti che quest'ultimo è più veloce e consente di ridurre la dimensione del file di dump (che contiene meno caratteri), ma la soluzione con INSERT risulta maggiormente portabile anche fra database differenti.

Si noti come lo script accetti i parametri relativi all'host al quale occorre connettersi e con quale username. Solitamente è bene specificare l'utente amministratore per il database, avendo cura di verificare il file pg_hba.conf affinché contenga un'entry che consenta la connessione dall'host dal quale si effettua il backup.

Qualora il database richieda una password per l'utente specificato, occorre specificare tale password mediante il file .pgpass, che deve trovarsi nella directory dell'utente che lancia lo script di backup. Il file .pgpass ha la seguente struttura:

host:porta:database:utente:password

come ad esempio

sede:5432:myDB:luca:L)c(
192.168.1.2:5432:postfix:Mailer:$escCTRL


A questo punto è possibile inserire la schedulazione dello script in cron, con ad esempio delle entry simili alle seguenti:

00 19 * * 5 backup_postgresql.sh   myDB luca     sede /backup/vari/postgres   --dated
00 19 * * 1-4 backup_postgresql.sh myDB luca sede /backup/vari/postgres
Si noti che per cinque giorni la settimana viene fatto il backup a rotazione settimanale (modalità single), mentre una volta alla settimana viene effettuato quello dated. In questo modo si ha un backup per ogni giorno della settimana, e un backup persistente per ogni settimana.

Programmazione ad oggetti

La programmazione ad oggetti (OOP = Object Oriented Programming) rappresenta un paradigma di programmazione molto potente, e uno di quelli maggiormente affermati negli ultimi anni.

La programmazione ad oggetti si basa fondamentalmente su tre principi:
  1. incapsulamento: è la capacità di nascondere all'esterno dati e servizi, inglobandoli in un modulo che risulti inviolabile dall'esterno.
  2. ereditarietà: è la capacità di estendere un modulo aggiungendo o modificando alcuni dei suoi servizi.
  3. polimorfismo: è la capacità di variare il comportamento di un modulo a seconda di come viene acceduto.
L'idea attorno al quale ruota l'OOP è il concetto di classe, un'astrazione di dato con annessi relativi servizi. La classe supporta i tre principi dell'OOP sopra descritti, e infatti:
  1. può nascondere i dettagli implementativi dei propri dati all'esterno (incapsulamento).
  2. può essere estesa da una classe figlia, che implementi ulteriori servizi (ereditarietà).
  3. può venire acceduta con ciascuna interfaccia appartenente ad una delle sue superclassi, ma il suo comportamento dipenderà dalla sua implementazione concreta (polimorfismo).
E' importante notare come, grazie all'ereditarietà, il codice nuovo possa usare il codice vecchio. Ma grazie al polimorfismo si ha che il codice vecchio utilizza quello nuovo. Nel primo caso (ereditarietà) si ha che da una implementazione "vecchia" se ne può ottenere una nuova definendo (o ridefinendo) solo i servizi necessari (e quindi utilizzando i rimanenti servizi vecchi). Nel secondo caso (polimorfismo) si ha che il codice "vecchio" che accede tramite un'interfaccia di una superclasse ad una implementazione nuova, sfrutta la nuova implementazione senza rendersene conto.

Per meglio chiarire, si consideri il seguente esempio:


public class Automobile{
// incapsulamento: la velocità e' nascosta all'esterno e può essere
// acceduta solo tramite opportuni servizi
protected int speed = 0;

// servizio pubblico per variare la velocita', si noti che il chiamante
// non conosce nessun dettaglio implementativo circa la velocita'
public void accelera(){ this.speed++; }
}



public class AutomobileSportiva extends Automobile {

// l'automobile sportiva utilizza come base Automobile

// Un'auto sportiva ha una accelerazione forte. Si noti che si ridefinisce
// un servizio che si basa sulle proprieta' del codice vecchio, ossia la variabile
// speed.

public void accelera(){
for(int i=0; i<4;>
this.accelera();
}

}



Si supponga che in un programma ci sia un metodo che utilizza l'automobile:


public void corri(Automobile auto){ auto.accelera(); }


Ora, è evidente che se a questo metodo viene passata un'istanza di AutomobileSportiva, il codice in questione (vecchio) utilizza il nuovo comportamento (polimorfismo). Ecco quindi che senza alterare il codice vecchio si è inserito un comportamento nuovo, e quindi il codice vecchio usa il codice nuovo. Similarmente, AutomobileSportiva rappresenta codice nuovo che utilizza quello vecchio: il suo metodo accelera() richiama il codice vecchio della superclasse.

Da notare come il comportamento ottenuto dipenda dal tipo run-time effettivo, ossia il polimorfismo mostra sempre l'ultimo comportamento definito nel tipo run-time fra quelli disponibili a partire dal tipo statico.

Si noti che il polimorfismo da solo non basta: è indispensabile disporre anche del principio di sostituzione di Liskov, che consente di utilizzare l'interfaccia di una superclasse per riferirsi ad una sottoclasse.

Windows in facoltà...è proprio necessario?

Non posso fare a meno di notare come molti laboratorio accademici siano dotati di computer con Microsoft Windows. Non solo, molti professori e ricercatori utilizzano il sistema operativo Microsoft nella loro attività quotidiana. Senza bisogno di scadere nella ormai banale guerra Microsoft-OpenSource, mi domando come mai, specialmente in facoltà a carattere tecnologico, si utilizzino sistemi a pagamento (Microsoft Windows è solo la punta dell'iceberg, poiché spinge all'uso di programmi a pagamento). E' evidente che ci sono ambiti in cui l'utilizzo di Microsoft Windows sia obbligato, e mi riferisco a tutti i casi in cui sia imposta quella tecnologia. Ma in tutti gli altri casi, è proprio necessario?

Per fare un corso di videoscrittura o di web-design, è necessario usare sistemi Microsoft? Nei corsi di programmazione C, Java, ecc., non si può utilizzare un sistema Linux? Beh, banalmente direi proprio di si.

Qual'è il vantaggio di passare a Linux? Ce ne sono diversi:
  • agli studenti viene presentato un mondo che spesso ignorano: l'alternativa OpenSource.
  • gli strumenti disponibili oggi nel panorama Linux (e degli Unix liberi) sono perfettamente competitivi con quelli disponibili nel mondo Microsoft.
  • l'utilizzo di strumenti OpenSource tende a stimolare la curiosità degli studenti.
  • si risparmia il costo delle licenze richieste da software Microsoft e a pagamento.
Ce ne sarebbe altre di motivazioni, ma quella che mi preme mettere in evidenza è proprio il risparmio sulle licenze. Banale, ma se si pensa che i costi di licenza incidono direttamente sui fondi disponibili ai gruppi di ricerca, nonché agli atenei in generale, non posso fare a meno di chiedermi perché. E non posso fare a meno di notare un certo menefreghismo sull'argomento, quasi non ci si renda conto di come questo fattore incida sui bilanci. E sulle tasse degli studenti!

Certo, molti laboratori adottano il dual boot, ma non basta. E' necessario un passaggio più radicale, lasciando i sistemi Microsoft solo ove realmente necessari. I docenti e i ricercatori per primi potrebbero dare il buon esempio. E gli studenti, la prossima volta che pagano le tasse, dovrebbero pensare anche a questo.

giovedì 7 febbraio 2008

Compilare un progetto con Ant usando AspectJ in Eclipse

La configurazione di Ant per la compilazione mediante AspectJ non è sempre semplice, in particolare se si usa Ant all'interno di Eclipse. Questo articolo illustrerà brevemente i passi principali per arrivare ad avere un ambiente funzionante. Si fa riferimento a Eclipse 3.3, Ant 1.7, AspectJ 1.5; nel seguito si mostreranno pezzi di un build file completo, che non viene riportato interamente per ragioni di spazio. Le variabili utilizzate sono state nominate in modo da rendere comprensibile la lettura anche senza avere accesso alla loro definizione.

Definizione del Task Ant
All'interno del file di progetto occorre definire il task iajc, che è l'equivalente del task javac normalmente usato in Ant.

<!-- where to find the aspectj jars required for the compilation -->
<property name="aspectj.tools" value="${src.lib.dir}/aspectjtools.jar"> </property>
<property name="aspectj.runtime" value="${src.lib.dir}/aspectjrt.jar" > </property>

<!-- definition of the ant task for the compilation -->
<taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties" classpath="${aspectj.tools}">
</taskdef>


Le prime due proprietà sono definite per comodità e modularità del build file. Si ricordi che per definire il task iajc occorre il jar aspectjtools.jar, mentre per la compilazione degli aspetti occorre il jar aspectjrt.jar (che fornisce accesso alle strutture run-time di AspectJ). Una volta inserita la definizione del nuovo task si può procedere con la compilazione vera e propria. Da notare che non sempre un taskdef permette l'inclusione di elementi annidati, ecco perché a differenza della guida di AspectJ si è definito il classpath come attributo e non come elemento.

Definizione del classpath
E' bene definire un elemento classpath da utilizzare ovunque nel proprio file di build, indipendentement dall'utilizzo o meno di AspectJ. Di seguito vi è un esempio di classpath per un progetto che richiede alcune dipendenze esterne (driver JDBC per PostgreSQL, Hibernate, Log4J, JUnit):

<!-- the classpath with specific libraries required by the project -->
<path id="build.classpath">
<pathelement location="${build.lib.dir}/aspectjrt.jar" />
<pathelement location="${build.lib.dir}/hibernate3.jar" />
<pathelement location="${build.lib.dir}/log4j-1.2.15.jar" />
<pathelement location="${build.lib.dir}/postgresql-8.3-603.jdbc4.jar" />
<pathelement location="${build.lib.dir}/junit-4.4.jar" />
</path>


Task di compilazione
E' giunto ora il momento di inserire il task di compilazione vero e proprio. Il task di compilazione, come già accennato, è chiamato iajc e prevede parametri simili a quelli del task javac. Da notare come ogni attributo path del task iajc possa essere specificato anche come elemento annidato (di tipo pathelement).
Di seguito la definizione del task di un task di compilazione:

<!-- AspectJ compilation of the project -->

<target name="_AspectJCompile" depends="init" description="Compiles the project using AspectJ compiler." >
<iajc srcDir="${src.dir}" destDir="${build.dir}" source="1.5" classpathref="build.classpath">
</iajc>
</target>


E' interessante notare come si faccia riferimento al classpath precedentemente definito e come siano impostati i percorsi sorgente e destinazione per il build. Da notare anche la compatibilità dei sorgenti (attributo source).


lunedì 4 febbraio 2008

Dalle tabelle ai file...

Come ogni buon RDBMS, PostgreSQL gestisce in modo trasparente i dati su supporto fisico (disco fisso), lasciando all'utente la cognizione del solo livello di gestione dei dati (relazioni, indici, ecc.). E' comunque possibile consultare i cataloghi interni di PostgreSQL per sapere dove e come vengano salvati i dati sul supporto fisico. In questo post verrà mostrato come trovare la corrispondenza fra dati e file su disco, e come calcolare lo spazio disco occupato da un database o da una tabella. Negli esempi che seguiranno si farà riferimento ad un database testdb che contiene la tabella clienti definita di seguito:

testdb=# \d clienti
Table "public.clienti"
Column | Type | Modifiers
--------+---------+------------------------------------------------------
pk | integer | not null default nextval('clienti_pk_seq'::regclass)
name | text |
address | text |
Indexes:
"clienti_pkey" PRIMARY KEY, btree (pk)

Per recuperare le informazioni relative a dove i dati sono memorizzati occorre consultare il catalogo di sistema. In particolare le tabelle che maggiormente interessano sono:
  • pg_database contiene le informazioni generali sul database (nome, proprietario, tablespace, encoding, ecc.);
  • pg_tablespace contiene le informazioni specifiche sui vari tablespace definiti (locazione su disco, proprietario, ecc.);
  • pg_roles contiene gli utenti definiti nel sistema e il loro id numerico;
  • pg_class contiene le informazioni circa le tabelle definite in ogni database.
La prima cosa da fare, per trovare il proprio database su disco, è quella di interrogare il catalogo dei database:

select db.oid,      -- identificativo del db
db.datname, -- nome simbolico del db
db.datdba, -- amministratore del db
r.rolname, -- nome dell'amministratore
db.dattablespace, -- identificativo del tablespace
ts.spcname, -- nome simbolico del tablespace
ts.spcowner, -- identificativo proprietario tablespace
r2.rolname, -- nome simbolico del proprietario
ts.spclocation -- locazione su disco
from (((pg_database db join pg_roles r on db.datdba = r.oid)
left join pg_tablespace ts on db.dattablespace = ts.oid)
join pg_roles r2 on ts.spcowner = r2.oid);

oid | datname | datdba | rolname | dattablespace | spcname | spcowner | rolname | spclocation
------+----------------+--------+----------------+---------------+-------------------+----------+----------+-----------------------------------
17318 | testdb | 16388 | luca | 1663 | pg_default | 10 | postgres |

Le colonne interessanti in questo caso sono pg_database.oid, che indica l'id del database stesso. In particolare, su disco esisterà una directory con nome pari all'id del database (e quindi 17318). Si noti come il database risulti contenuto nel tablespace di default (spcname = pg_default) e per questi non sia indicata una location particolare. Questo significa che la directory che contiene i file del database si troverà sotto $PGDATA/base/17318. In effetti si ha che:

ls -lhd $PGDATA/base/17318/
drwx------ 2 postgres postgres 4,0K 2008-02-04 11:26 17318/

All'interno della directory 17318 sono contenuti una serie di file, generalmente uno per oggetto del database. Ad esempio, ogni tabella sarà identificata nei cataloghi tramite un OID numerico che troverà corrispondenza in un file su disco con nome pari allo stesso OID. Ad esempio, per la tabella clienti si ha che:

testdb=# select oid,relname, relpages, reltuples from pg_class where relname='clienti';
oid | relname | relpages | reltuples
------+---------+----------+-----------
17321 | clienti | 291 | 36864

La cosa interessante da notare è che la tabella clienti ha un OID 17321 e che occupa su disco 291 pagine per 36864 tuple (lo stesso dato si ottiene da un count sulla tabella).

Ipotizzando una dimensione delle pagine di 8kB (il valore dipende dall'installazione di PostgreSQL), si può usare il numero di pagine per capire la dimensione del file/tabella su disco:

testdb=# select relname,
(relpages * 8 * 1024) as size_bytes,
( (relpages * 8 * 1024) / (1024 * 1024) ) as size_MB
from pg_class where relname='clienti';

relname | size_bytes | size_mb
--------+------------+---------
clienti | 2383872 | 2

Come si nota, ci si aspetta una dimensione di 2,3 MB su disco per la tabella 17321, e in effetti si ha che:

ls 17318/17321 -lh
-rw------- 1 postgres postgres 2,3M 2008-02-04 11:32 17318/17321

Riassumendo quindi si ha che:
  1. ad ogni database viene associato un OID numerico, memorizzato nel catalogo di sistema. Tale OID corrisponde ad una directory su disco (all'interno di $PGDATA/base se non si ha un tablespace particolare, altrimenti la directory è contenuta nel tablespace);
  2. ogni oggetto di database, ad esempio una tabella, ha a sua volta un OID che corrisponde ad un file su disco con nome pari all'OID.
Esistono anche funzioni appositamente studiate per visualizzare la dimensione di un database o di una tabella, in particolare:
  • pg_database_size('database_name') mostra la dimensione in bytes del database specificato;
  • pg_relation_size('relation_name') mostra la dimensione dei soli dati della relazione specificata;
  • pg_total_relation_size('relation_name') mostra la dimensione complessiva di una relazione, inclusi i dati e gli indici;
  • pg_size_pretty(value) formatta un valore in un formato umanamente leggibile.
Quindi, per sapere la dimensione del database si può eseguire:

testdb=# select pg_database_size('testdb'),
pg_size_pretty(pg_database_size('testdb'));

pg_database_size | pg_size_pretty
-----------------+----------------
6915800 | 6754 kB
Il primo valore è in bytes, il secondo è in kB formattato automaticamente tramite la pg_size_pretty. Analogamente, per sapere la dimensione di una relazione si ha:

testdb=# select pg_size_pretty( pg_relation_size('clienti') ),
pg_size_pretty( pg_total_relation_size('clienti') );
pg_size_pretty | pg_size_pretty
----------------+----------------
2328 kB | 3000 kB

Si noti come la dimensione dei soli dati corrisponde a 2,3 MB come già evidenziato, mentre si considerano anche gli indici si ha una dimensione di 3 MB.

Si tenga presente che, per ragioni di praticità ed efficienza, PostgreSQL divide i dati di una relazione in gruppi da 1GB. Questo significa che se una relazione aumenta oltre 1GB, il relativo file su disco viene diviso anch'esso in blocchi da 1GB l'uno. Si supponga di aver aumentato la tabella clienti in modo da superare 1 GB di soli dati:


testdb=# select pg_size_pretty( pg_relation_size('clienti') ),
pg_size_pretty( pg_total_relation_size('clienti') );
pg_size_pretty | pg_size_pretty
---------------+----------------
1161 MB | 1484 MB

Su disco quello che è successo è che il file 17321 è stato diviso in due, nello specifico un 17321 (che contiene il primo GB di dati) e un 17321.1 (che contiene il secondo GB di dati). Qualora anche il secondo file raggiunga il suo limite, PostgreSQL provvederà a costruire un file 17321.2 e così via. A livello di catalogo di sistema questa cosa non viene mostrata, ovvero non esiste nella tabella pg_class un'entry per ogni file creato su disco, ma resta solo l'entry della relazione 17321 con il numero di pagine e tuple chiaramente aumentato.

ls -lh 17318/17321*
-rw------- 1 postgres postgres 1,0G 2008-02-04 13:51 17318/17321
-rw------- 1 postgres postgres 138M 2008-02-04 13:52 17318/17321.1

Una considerazione finale d'obbligo: sebbene si sia mostrato come risalire alla locazione fisica dei dati gestiti dal database si ricorda come manipolare manualmente tali dati possa risultare pericoloso per la stabilità e la coerenza dei dati stessi. E' quindi bene lasciare al database la gestione della corrispondenza fra lo spazio fisico e quello logico.

venerdì 1 febbraio 2008

Inserire le informazioni di build in un'applicazione Java

Una funzionalità molto comoda di Ant è quella di poter filtrare i file di testo (ivi inclusi i sorgenti Java) durante il processo di build. Tale filtraggio, ottenuto tramite i datatype FilterSet può essere utilizzato agevolmente per inserire le informazioni di build nel proprio applicativo, ad esempio nel titolo di una finestra o in una finestra di dialogo.

Nell'esempio che riporto di seguito si andrà ad inserire il numero e la data di compilazione in una stringa specifica, contenuta in un file di proprietà. L'utilizzo di un file di proprietà con le stringhe e i messaggi da mostrare all'utente è comodo soprattuto perché consente l'internazionalizzazione. Tuttavia, è possibile filtrare direttamente un file .java che contenga la stringa equivalente.

Si supponga di avere un file di proprietà che contenga, fra le altre, la proprietà che imposta il titolo della finestra principale dell'applicazione:

application.title = My Application @APPLICATION_VERSION@

Come è facile intuire il titolo dell'applicazione è formato da una parte statica e immutabile ("My Application") e da una variabile ("@APPLICATION_VERSION@"), quest'ultima che dovrà essere sostituita a tempo di compilazione con le informazioni di build. In sostanza, una volta terminato il processo di build si avrà qualche cosa di simile a:

application.title = My Application [ build number 637 - 01/01/2008 ]


Per raggiungere lo scopo occorre filtrare con Ant il file di proprietà al momento in cui viene fatto il build. E' buona norma, all'atto di un build, creare uno spazio di build che sia identico (o simile) a quello in cui risiedono i sorgenti. Pertanto, immaginando di avere tale file di proprietà in una directory di configurazione e di volerlo copiare, assieme ad altri file di confifgurazione, nella directory relativa di build, il task da utilizzare è il seguente:


<target name="_copyConfig" depends="_createDirectoryStructure" >

<copy todir="${build.conf.dir}" >
<fileset dir="${src.conf.dir}">
</fileset>

<!-- filter the config files -->
<filterset>
<filter token="APPLICATION_VERSION"
value="[ build number ${build.number} - ${build.timestamp} ]" />
</filterset>

</copy>
</target>



Brevemente quello che avviene è che tutti i file dalla directory di configurazione di origine (${src.conf.dir}) vengono spostati in quella di build (${build.conf.dir}), e nel fare questo i file vengono filtrati dal FIlterSet: ogni file che contiene la stringa @APPLICATION_VERSION@ viene modificato e al posto di quest'ultima si inseriscono le informazioni di build.

Si noti che il task di cui sopra fa riferimento a due variabili: ${build.number} e ${build.timestamp}. Entrambe le variabili devono esistere al momento del filtraggio, e quindi devono essere state definite precedente (all'inizio del file di build o in un task precedente quello corrente). La variabile ${build.number} viene impostata tramite il task buildnumber, mentre quella ${build.timestamp} viene generata tramite il task di timestamp. In sostanza:


<buildnumber/>
<tstamp>
<format property="build.timestamp"
pattern="dd/MM/yyyy"/>
</tstamp>




In conclusione Ant può essere utilizzato in modo molto efficace e flessibile per inserire informazioni di build all'interno della propria applicazione. L'esempio qui riportato ha illustrato il concetto, altre casistiche e casi d'uso più complessi sono ovviamente possibili.