Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Versione 1.0: release candidate

Introduzione

La AI di Google™ definisce una Release Candidate come:

Una release candidate (= candidata al rilascio) è una versione pre-rilascio di un software che è molto vicina alla release finale, ma potrebbe essere ancora sottoposta a test finali e correzioni di bug prima della disponibilità generale. È una fase in cui il software è sostanzialmente completo, con tutte le funzionalità principali implementate, ma potrebbe presentare ancora alcuni problemi minori da risolvere.

Benchè tutte le funzionalità che avevamo previsto nella roadmap iniziale (vedi La lista delle versioni) sono state implementate, in questa versione finale aggiungiamo quegli "accessori" che completano una applicazione.

I nuovi files sorgente

Nella seguente tabella trovate l'elenco dei files sorgente che andrò a commentare e che fanno parte della nuova versione.

nome file package descrizione
ConnectionTest10.java mastermind.net.impl implementa un eastern egg
MasterMindServer10.java mastermind.game implementa una backdoor
Main10.java mastermind.app la classe principale della versione 1.0

La persistenza delle proprietà

Il giocatore può modificare diverse proprietà della applicazione attraverso il pannello delle proprietà (vedi Versione 0.7: Il pannello delle proprietà) tuttavia egli si aspetta anche che le modifiche apportate persistano. Il giocatore trova tedioso dover modificare le proprietà ad ogni avvio della applicazione specialmente se le sue preferenze nei parametri di gioco sono diverse da quelle di default.

La classe MMProperties possiede due membri dati che possono essere usati per questo scopo; uno di essi scrive le proprietà su un file su disco mentre l'altro legge le proprietà da un file su disco:

public void writeToFile(String filename)
public void readFromFile(String filename)

Dobbiamo solo decidere quale nome di file usare e in che momento richiamare i due metodi dal Main.

Il nome del file delle proprietà

Ovviamente il nome del file è a piacere del programmatore e quello che ho scelto io per il file delle proprietà è: mastermind.properties: un nome che fa capire al volo il suo contenuto. Ora debbiamo scegliere la cartella in cui posizionarlo. Vi sono sostanzialmente due strade da seguire:

  • nella stessa cartella della applicazione: questo però è possibile solo se vi è un solo user che usa la applicazione; se gli user sono due ed il file delle proprietà è uno solo si rischia che gli user sovrascrivano le proprietà l'uno dell'altro
  • nella cartella personale di ogni user che userà la applicazione

Delle due, la scelta migliore è senza alcun dubbio la seconda. Il runtime di Java mette a disposizione il metodo statico System.getProperties che ritorna un oggetto di classe Properties nel quale vengono elencate una serie di proprietà di sistema come per esempio il sistema operativo che equipaggia la macchina, la sua versione, etc.
Una di queste proprietà è la home directory (la cartella di casa) dello user che ha eseguito la applicazione e si può ottenere con il seguente codice:

String home = System.getProperty( "user.home" );

Pertanto, è facile ottenere il percorso completo del file delle proprietà per ogni user che gioca concatenando la home del user con il nome del file mastermind.properties. Il nome del file che contiene le proprietà viene determinato nel costruttore della classe MMProperties.

Le modifiche al Main

Le proprietà vanno lette dal file allo stratup della applicazione: il luogo più consono per questa operazione è il metodo overrideDefaultProperties che viene richiamato da run subito dopo che la classe MMProperties è stata istanziata ma prima di interpretare la command-line.
Per quanto riguarda la scrittura delle proprietà sul file, il metodo più logico dove eseguirla è il terminate considerato che esso viene sempre richiamato quando la app stà per terminare. Non riporto il codice poichè non servono commenti; potete leggerlo facilmente nel file sorgente Main10.java.

Chiusura della applicazione

Nella versione 0.4 della applicazione abbiamo introdotto la interfaccia Application (vedi La interfaccia Application) che viene implementata solo dalla classe Main. Oltre a gestire i giocatori, il frame principale e la creazione di nuove partite definiva anche il metodo terminate il quale, come intuibile, termina la applicazione.
In quella versione primitiva, il metodo era piuttosto semplice:

// File: Main04.java
/* Needed by Application interface */
public void terminate()
{
System.exit(0);
}

Con il passare delle versioni, il metodo terminate è diventato molto più complesso ed in questa versione RC è chiamato a diversi compiti:

  • scrivere le proprietà della applicazione sul file
  • chiudere la eventuale connessione
  • fermare l'invio e la ricezione dei pacchetti multicast UDP, se ancora in corso

Benchè i moderni sistemi operativi rilasciano tutte le risorse (memoria allocata, flussi e canali aperti) quando un processo termina, è buona abitudine di programmazione procedere alla chiusura dei flussi e dei canali direttamente nel codice. Il metodo terminate viene richiamato da diversi punti della applicazione:

  • dal bottone di comando Exit a fine partita se il giocatore non intende giocare una nuova partita
  • dal bottone di comando Abort del pannello della connessione quando il giocatore non intende proseguire
  • dal bottone di comando Abort nella dialog-box che visualizza un messaggio di errore di connessione

Ma ci è sfuggito un evento; la chiusura della applicazione tramite il bottone di chiusura del frame, quello che sta in alto a destra in ogni frame principale. Ovviamente, non possiamo trascurarlo; il metodo terminate và richiamato sempre quando la applicazione termina.

Per ottenere questo dobbiamo intercettare l'evento WindowClosing che viene accodato nella coda degli eventi quando una applicazione sta per terminare. Per intercettare l'evento dobbiamo:

  • implementare la interfaccia WindowListener
  • aggiungere la classe implementante come listener dell'evento
  • modificare il comportamente di default del bottone di chiusura del frame

La interfaccia WindowListener ha molti metodi da implementare:

  • windowActivated: Invoked when the Window is set to be the active Window.
  • windowClosed: Invoked when a window has been closed as the result of calling dispose on the window.
  • windowClosing: Invoked when the user attempts to close the window from the window's system menu.
  • windowDeactivated: Invoked when a Window is no longer the active Window.
  • windowDeiconified: Invoked when a window is changed from a minimized to a normal state.
  • windowIconified: Invoked when a window is changed from a normal to a minimized state.
  • windowOpened: Invoked the first time a window is made visible

e dovremmo implementarli tutti anche se noi siamo interessati al solo evento windowClosing. Per evitare di scrivere tutti gli altri metodi vuoti ci viene in aiuto la classe WindowAdapter: essa implementa tutti i metodi di WindowListener senza fare alcuna operazione nel metodo implementante. Purtroppo, non possiamo derivare Main10 da WindowAdapter poichè la classe Main10 già è una derivata di Main09 e Java non permette ad una classe di derivare da più di una classe base.
Dobbiamo quindi implementare tutti i metodi della interfaccia WindowListener anche se siamo interessati ad uno solo di essi? Non necessariamente, una soluzione esiste ed è quella di derivare da WindowListener il capostipite della applicazione principale e cioè la classe Main. Quest'ultima non sovrascrive alcuno dei metodi di interfaccia quindi si comporta esattamente come prima e tutte le versioni precedenti si comporteranno esattamente come quando le avevamo scritte.

// File: Main.java
public class Main extends WindowAdapter implements .....
{
... omissis ...
}

Con questo accorgimento abbiamo ottenuto che, comunque, la classe Main10 deriva, anche se indirettamente, da WindowAdapter e possiamo pertanto disinteressarci degli eventi WindowEvent che non riguardano la chiusura della applicazione; possiamo implementare il solo metodo di interesse. Dobbiamo anche modificare, nel metodo run, il comportamento di default del bottone "chiusura finestra" del frame:

// File: main10.java
public class Main10 extends Main09
{
... omissis ...
@Override
public void windowClosing( WindowEvent evt )
{
// se premuto il bottone "chiudi finestra" richiama terminate()
terminate();
}
@Override
protected void run( String[] argv ) throws MMException
{
... omissis ...
// per intercettare l'evento WindowsClosing non si deve usare il
// comportamento di default EXIT_ON_CLOSE del frame
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.addWindowListener( this );
... omissis ...
}
}

In questo modo, abbiamo la certezza che, quando la applicazione termina, il metodo terminate viene sempre correttamente eseguito e le risorse allocate saranno tutte liberate.

Eastern Egg

Un easter egg (= uovo di Pasqua) in informatica è un contenuto, di solito di natura faceta o bizzarra e innocuo, che i progettisti o gli sviluppatori di un prodotto, specialmente software, nascondono nel prodotto stesso (vedi Easter Egg per maggiori informazioni).

La applicazione Mastermind non sarebbe completa senza un eastern egg: normalmente, gli eastern eggs sono nascosti nella applicazione e quindi non andrebbero assolutamente documentati ma questo è un tutorial e quindi non soggiace alle regole non scritte dello sviluppo software.
Il codice che implementa l'eastern egg in Mastermind è contenuto nel file sorgente ConnectionTest10.java ma non lo commenterò (è di facile lettura) tuttaiva, vi svelerò come attivarlo: selezionando un avversario remoto e collegandosi alla porta 2112 (21 dicembre) del server ConnectionTEST, nel messaggio di presentazione inviato dal remoto fittizio si otterrà una citazione del cantautore statunitense Frank Zappa.

Le backdoors

Mentre un eastern egg è innocuo, le backdoors (=porte sul retro) sono metodi nascosti che consentono l'accesso a un sistema, una rete o un'applicazione software, aggirando i normali meccanismi di sicurezza. Questo accesso può essere utilizzato sia da utenti autorizzati, ad esempio per scopi di manutenzione, sia da malintenzionati per scopi illeciti.

Essendo un giochino per bambini, non esiste alcun accesso remoto nel Mastermind ma in questa versione finale è stato nascosta una specie di "porta sul retro" che permette ad un giocatore di barare: inviando una guess (ipotesi di soluzione) pari a "000" (se k=3) oppure "0000" (se k=4) il server notificherà al giocatore che ha inviato la sequenza, la solution (sequenza segreta) dell'avversario che sarà visualizzata sulla riga della solution nel pannello delle sequenze (vedi Il pannello delle sequenze).
La backdoor viene elaborata nel metodo del server haveGuess:

// File: MasterMindServer.java
@Override
public void haveGuess(int id, String guess, boolean gui )
{
... omissis ...
try {
// introduce la backdoor
if ( "000".equals( guess ) || "0000".equals( guess )) {
Player p = app.getPlayerManager().getPlayer( turn );
assert p != null : "MasterMindServer10.haveGuess() variable \'p\' is null";
if ( p.isRemote()) {
p.notifySolution( turn^1, solutions[turn^1] );
}
else {
notifySolution( turn^1, solutions[turn^1] );
}
return;
} // fine backdoor
... omissis ...
}

Che altro dire sulle backdoors? Beh, possono essere strumenti molto utili per monitorare una applicazione; creando un piccolo server in stile telnet (vedi Un piccolo telnet) è possibile inviare comandi specifici alla applicazione stessa mentre essa è in esecuzione. Considerata la enorme, gigantesca falla nella sicurezza usando un telnet come quello presentato in questo tutorial, è necessario mitigare il rischio di intrusioni indesiderate con un trucchetto che consiste nel verificare, da parte del server telnet, l'indirizzo IP di chi si è collegato e pretendere che sia solo il localhost.

ServerSocket server = new ServerSocket(port);
Socket client = server.accept();
InetAddress address = client.getRemoteSocketAddress().getAddress();
if ( !address.isLoopbackAddress()) {
client.close(); // chiude la connessione se il client non è localhost
}

Con questo accorgimento, solo chi ha accesso fisico alla macchina dove la applicazione è in esecuzione si può collegare tramite il piccolo server telnet e se un malintenzionato ha accesso fisico alla macchina il nostro piccolo telnet non costituisce certo un problema: ci sono problemi ben più gravi da risolvere.

Archivio Java

Quando la applicazione è pronta per il rilascio è consigliato creare un unico archivio che contiene tutto il necessario per la sua esecuzione. Questo archivio deve sempre includere almeno:

  • tutte i files .class che contengono il bytecode
  • tutte le risorse come per esempio le immagini che devono essere visualizzate nella applicazione
  • le eventuali librerie di terze parti se sono state usate nella applicazione

Benchè sia possibile usare strumenti come 7-Zip per creare un archivio, nel caso di una applicazione Java è preferibile usare lo strumento specifico: il Java Archiver (JAR) che crea un unico file contenente la applicazioe e che, al conreario del file compresso generato da un semplice compressore, può essere eseguito dalla JVM. Mi riferirò a questo tipo di file col nome: jarfile. Il JDK mette a disposizione uno strumento CLI per creare i jarfiles; per verificarne la disponibilità sul vostro sistema, digitate:

jar --version

Dovreste ottenere la versione installata sul vostro computer. Cosa inseriamo nel jarfile oltre a quanto elencato sopra? Normalmente, le applicazioni Java contengono solo i files compilati, cioà i files .class ma, se la applicazione è open source tanto vale inserire i sorgenti nel jarfile stesso.

In pratica, possiamo inserire nel jarfile tutto ciò che si trova nella cartella mastermind e in tutte le sue sottocartelle. Anche la cartella docfiles dovrebbe essere inserita nel jarfile poichè essa contiene i files sorgente per produrre la documentazione javadoc delle diverse versioni della applicazione. Non dovrebbe, invece, essere inserita nel jarfile la cartella docs che contiene la documentazione tecnica delle classi: il contenuto della cartella non è interessante per l'user (cioè per il giocatore) e, comunque, può essere generato dal comando javadoc.

Creare un archivio Java

In generale, per creare un jarfile si usa il comando in questo modo:

jar cvfe jar-file entry-point input-file(s)

dove cvfe sono opzioni il cui significato è il seguente:

  • "c" sta per --create ed è usato per creare un jarfile
  • "v" sta per --verbose ed è usato per avere un output verboso delle operazioni eseguite dal comando jar
  • "f" sta per --file ed è usato per creare il jatfile il cui nome è specificato nell'argomento jar-file; in assenza di questa opzione, l'output del comando jar viene diretto sullo standard output (il terminale)
  • "e" sta per --main-class ed è usato per specificare il entry-point della applicazione cioè la classe che contiene il metodo statico main

Quindi il comando per creare il jar-file di mastermind è il seguente:

>jar cvfe mastermind.jar mastermind.app.Main mastermind/ docfiles/

Eseguire la applicazione

Per eseguire la applicazione impacchettata nel jarfile si usa sempre il comando java ma con una opzione specifica:

>java -jar mastermind.jar

il comando può essere seguito dagli argomenti della command-line che contiene le coppie "chiave=valore" come, peraltro già si poteva fare in precedenza. Per esempio:

>java -jar mastermind.jar version=2048 opponentType=AI

Ulteriore documentazione

La documentazione completa delle features descritte in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc del progetto: The MasterMind Project Version 1.0

Conclusioni

Il giochino è completo ma, come riportato in Introduzione una release candidate non è del tutto finita. Essa non è destinata al grande pubblico ma di solito ad una cerchia ristretta di amici e clienti per un test esaustivo che potrebbe perdurare anche parecchio tempo.
In questo contesto le filosofie di rilascio sono contrastanti. Mentre il software commerciale segue la regola descritta poc'anzi, il software libero, (inteso come open source non come "gratis") usa un approccio completamente diverso e segue la filosofia che in inglese viene chiamata release soon, release often che significa "rilascia presto, rilascia spesso".
Alla base di questo approccio vi è la convinzione che se il software è rilasciato subito al grande pubblico vi sono molti più occhi che possono testare la applicazione e scovare i bugs. Non appena trovato un bug lo sviluppatore lo corregge e rilascia immediatamente una versione bug-fix della applicazione: ecco perchè i rilasci si susseguono molto spesso nel software libero.

Versioni successive

Premetto che non sono previste versioni successive alla 1.0 salvo, ovviamente, le versioni bug-fix se dovessero rendersi necessarie. Tuttavia, proprio mentre ero in procinto di pubblicare questa mia opera, ho avuto una idea per aggiungere una nuova funzionalità alla applicazione.

Per facilitare la ricerca dei giocatori in rete viene usato il protocollo UDP che invia ad un client Mastermind le informazioni essenziali al collegamento col server (vedi Version 0.9: la ricerca dei giocatori in rete). Le informazioni inviate nel payload UDP sono sostanzialmente due: l'indirizzo IP (o hostname) del server e la porta TCP/IP a cui collegarsi.
Come informazione aggiuntiva viene inviato anche il nickname del giocatore che funge da server. Si potrebbe aggiungere al payload UDP altre due informazioni:

  • il numero di versione della app server in modo che il client verifichi che sia compatibile: in caso negativo, è inutile aggiungere quella macchina alla lista dei server selezionabili nel pannello delle proprietà poichè il tentativo di connessione fallirebbe comuqne
  • i parametri di gioco scelti dal giocatore server: quando il client seleziona quel particolare server dalla lista nel pannello delle proprietà, la app potrebbe visualizzare nella scheda dei parametri di gioco quelli in uso in quel server; il giocatore client potrebbe quindi scegliere un determinato server in base ai parametir di gioco che più incontrano i suoi desideri

Non ho alcuna previsione di implementare questa nuova funzionalità nella app presentata in questo tutorial: lascio questo compito al lettore.

Argomento precedente - Argomento successivo - Indice Generale