MokaByte 60 - Febbraio 2002 
Il mondo java embedded
II parte: la Connected Limited Device Configuration
di
Marco Tomà
In questo articolo trattiamo la CLDC, una delle configurazioni previste da Java 2 Micro Edition. Analizzeremo le sue caratteristiche principali, le librerie e le differenze rispetto a Java 2 Standard Edition

La configurazione CLDC
L
l mese scorso, nelle pagine di Mokabyte, abbiamo incominciato ad occuparci di J2ME introducendo i concetti fondamentali dell'architettura, e i suoi elementi costitutivi: configurazioni e profili. In questo numero ci occuperemo della configurazione CLDC (Connected Limited Device Configuration), studiata appositamente per dispositivi dalle risorse hardware limitate (capacità computazionale, dotazioni di memoria, tipologia e dimensioni dei display, connessioni a reti di TLC); si tratta tipicamente di dispositivi portatili (molto spesso tascabili) dalle ridotte dimensioni come telefoni cellulari, smart-phone, PDA (Personal Digital Assistant), Pager comunicator, ecc., non a caso, infatti, i profili previsti per questo tipo di configurazione sono il PDAP (Personal Digital Assistant Profile) e il MIDP (Mobile Information Device Profile), dedicati al mondo palmare, alla telefonia cellulare e a dispositivi wireless in generale. Individuato il target dei dispositivi alla quale la CDLC mira, vediamo quali sono i requisiti minimi richiesti. La CLDC può essere utilizzata in apparati con:

  • almeno 160 Kbyte di memoria: 128 Kbyte di memoria non volatile (per la macchina virtuale e le librerie CLDC )
  • non meno di 32 Kbyte di memoria volatile (per il sistema runtime e l'allocazione degli oggetti Java)
  • processori RISC/CISC a 16-32 bit;
  • connettività a reti di telecomunicazioni, sovente di tipo wireless con larghezza di banda anche inferiore a 9600 bps;
  • un sistema operativo che gestisca l'hardware sottostante e possieda una scheduler per mandare in esecuzione la macchina virtuale. Tale SO può essere, quindi, anche molto semplice, ed essere privo di funzionalità avanzate come il multitasking o un file system di tipo gerarchico (molti dei dispositivi ai quali la CLDC è indirizzata hanno SO privi del concetto di file system).

Come previsto dalle specifiche J2ME, la configurazione CLDC si occupa di problematiche del tutto generali a prescindere dal dispositivo sul quale viene utilizzata, lasciando ai profili la gestione di tutti quegli aspetti "più strettamente legati alla piattaforma" (intesa come tipo di dispositivo es. Cellulare o PDA, non come marca - modello dell'apparato!).

In particolare la CLDC si occupa di:

  • definire le caratteristiche del linguaggio Java e della Virtual Machine supportate;
  • fornire un set minimo di librerie di base;
  • gestire gli stream di I/O;
  • sicurezza;
  • networking;
  • internazionalizzazione;

lasciando ai profili:

  • la gestione del ciclo di vita delle applicazioni (installazione, lancio, cancellazione);
  • l'implementazione di interfacce utenti;
  • la "cattura" e la gestione degli eventi;
  • l'interazione tra l'utente e l'applicazione;


Vediamo in dettaglio le caratteristiche (e le differenze rispetto alla Standard Edition) della configurazione CLDC.

 

Il package java.lang e le caratteristiche del linguaggio supportate
Il package java.lang, contiene il "DNA del linguaggio Java" quindi la sua presenza nella CLDC non può certo stupire, ma è del tutto ovvia. Tuttavia esso presenta alcune importanti differenze rispetto all'omonimo package della Standard Edition. La quasi totalità di tali differenze é dovuta alle caratteristiche dei dispositivi sui quali la CLDC dovrà funzionare, che avendo risorse limitate, hanno richiesto una "rivisitazione" dei package della Standard Edition ed un loro adattamento a questo tipo di piattaforme. Confrontando i package java.lang della CLDC e della Standard Edition una delle prime cose che colpisce è l'assenza nella CLDC delle classi Float e Double che costituiscono la "rappresentazione oggetto" dei tipi semplici float e double. La loro mancanza é dovuta al fatto che la maggior parte dei dispositivi, ai quali la CLDC è rivolta non ha il supporto hardware per i floating point e anche una loro "emulazione software" sarebbe stata troppo costosa in termini di risorse richieste ragion per cui è stata decisa la loro eliminazione dal set di base delle librerie CLDC. In realtà esistono package di terze parti (es. MathFP) che permettono l'utilizzo di tali tipi semplici (float e double) estendendo, inoltre, il set di metodi della classe Math. Quest'ultima, infatti, è particolarmente "impoverita", proprio a causa della mancato supporto dei numeri in virgola mobile e, si limita a fornire metodi per ottenere il massimo (max) o il minimo (min) tra due numeri (interi o long) e il valore assoluto (abs).
La CLDC, inoltre, non supporta daemon thread e gruppi di thread: poiché il multithreading è comunque garantito è possibile utilizzare gruppi di thread ma tutte le operazioni su di essi (start, stop, sleep, ecc.) devono essere eseguite in modo indipendente.
Per ragioni di sicurezza, ma anche a causa delle risorse richieste (troppo elevate), non è previsto il supporto del codice nativo (Java Native Interface). Le librerie native risultano quindi accessibili solo alla macchina virtuale, questo impedisce alle applicazioni qualunque tipo di accesso "esterno", garantendo quindi la loro esecuzione in un ambiente chiuso e protetto ("Sandbox Model"). (In realtà esistono macchine virtuali di terze parti, che consentono l'utilizzo del codice nativo, ad esempio J9 di IBM). Sempre per ragioni legate alla sicurezza e alle restrizioni imposte dal "Sandbox model" adottato, di cui parleremo più avanti, la CLDC non prevede la possibilità di utilizzare caricatori di classi definiti dall'utente. Il caricamento delle classi è gestito unicamente dalla virtual machine il cui class loader non può essere ridefinito a livello di applicazione.
Il ridimensionamento delle "classi di errore" è un'altra delle caratteristiche della CLDC, che emerge dal confronto con la Standard Edition. Le condizioni di errore che la CLDC si limita a rilevare riguardano mal funzionamenti della macchina virtuale (VirtualMachineError) o l'impossibilità di allocare memoria (OutOfMemoryError), nulla di più. Questo é dovuto al fatto che un'implementazione completa della casistica di errori che ritroviamo nella Standard Edition, (come indicato dalle specifiche del linguaggio java - Java Language Specification), richiederebbe risorse troppo elevate. Occorre poi tener presente che le condizioni di errore sono associate ad eventi solitamente irrecuperabili e, negli apparati embedded e nell'elettronica di consumo sono altamente legati al tipo di dispositivo, da qui la decisione di limitare le classi "error" ai due soli casi che abbiamo citato.
In ultimo (ma non per importanza) va detto che la CLDC non possiede quelle caratteristiche di "introspezione" proprie del linguaggio java, che ritroviamo nella Standard Edition. La CLDC, infatti, non supporta la Java Reflection, cioè la possibilità data alle applicazioni Java di "guardarsi dentro", ed ottenere informazioni sulle proprie classi, su costruttori, su metodi e attributi, nonché la possibilità di invocare tali metodi o modificare gli attributi in fase di runtime senza possedere alcuna informazione in fase di compilazione. Un'immediata conseguenza di tutto ciò è la mancanza della serializzazione (non troverete mai "implements Serializable" in un programma che utilizza la CLDC) che a sua volta comporta l'impossibilità di supportare RMI (Remote Method Invocation).

 

Classi di utilità: java.util
Anche le classi presenti nel package java.util hanno subito un notevole ridimensionamento rispetto alle corrispondenti classi di utilità presenti nell'edizione standard. Naturalmente, anche questa limitazione, é motivata dal fatto che nella configurazione CLDC è stato inserito solo "lo stretto necessario" per poter mantenere il "core" il più piccolo possibile e renderlo adatto a dispositivi con pochi kilobyte di memoria. Ecco quindi che in java.util troviamo davvero poco: l'interfaccia Enumeration, Hashtable, Vector e Stack per quel che riguarda le "collezioni di oggetti", Calendar, Date e TimeZone per la gestione e la manipolazione di data e ora. La classe Random per la generazione di numeri pseudocasuali completa il set delle classi di utilità di cui possiamo disporre.
Tra gli "strumenti" mancanti quelli di cui si avverte più la mancanza riguardano la manipolazione e il trattamento delle stringhe (lo StringTokenizer ad esempio) e un maggior supporto per l'internazionalizzazione: l'inserimento delle classi Locale, DateFormat e ResourceBoundle potevano rendere più completo il package senza peraltro aumentarne eccessivamente le dimensioni.

 

Gestione degli stream : java.io
L'input e l'output di Java si basano sul concetto di stream, cioè un generico flusso di byte "in transito" da una sorgente verso una destinazione indipendentemente dal tipo di sorgente, di destinazione e di percorso. Il package java.io non poteva quindi mancare nella CLDC, esso costituisce un sottoinsieme dell'omonimo della Standard Edition. In particolare nella CLDC sono state eliminate tutte le classi riguardanti le operazioni su file, questo perché la maggior parte dei dispositivi che utilizzeranno la CLDC, come già abbiamo detto, non possiedono un vero e proprio file system (i palmari palmOS ne sono un esempio, ma anche i cellulari e gli smart-phone). Le operazioni di I/O da/su file sono comunque garantite dal "Generic Connection Framework" di cui parleremo tra breve. Le due classi, InputStreamReader e OutputStreamWriter, rappresentano l'unico supporto all'internazionalizzazione di cui la CLDC dispone e permettono la conversione di sequenze di byte in caratteri Unicode e vicerversa:

InputStreamReader(InputStream is, String encoding);
OutputStreamWriter(OutputStream os, String encoding);

La CLDC supporta di default la codifica ISO-8859_1 come è possibile vedere "interrogando" le proprietà di sistema (System.getProperty("microedition.encoding")) altri tipi di codifica possono, però essere presenti in implementazioni particolari. Il mancato supporto per una codifica richiesta lancerà, ovviamente, una UnsupportedEncodingException.

 

Il "Generic Connection Framework"
Come è noto i package java.io e java.net nella Standard Edition forniscono un insieme completo di strumenti per la gestione dell'Input - Output sia per quanto riguarda l'I/O da file sia per quello legato a connessioni di rete. Sfortunatamente però hanno dimensioni tali (circa 200 Kbyte) da impedirne "la completa importazione" nella CLDC, inoltre molte delle funzionalità che garantiscono sono poco (o per nienete) utili in dispositivi embedded e nell'elettronica di consumo in generale. Inoltre molti di questi apparati dispongono di metodi di comunicazione "inusuali" e molto specifici (infrarossi, bluetooth), alcuni hanno un vero e proprio file-system altri hanno particolari "meccanismi" per il salvataggio persistente dei dati.
Occorreva quindi estrapolare dalla Standard Edition (java.io, java.net) le funzionalità principali e adattarle a questo contesto garantendo, al contempo, un elevato grado di estensibilità e flessibilità per garantire il supporto delle differenti forme di comunicazione e di nuovi protocolli.
E' stato quindi pensato quello che viene definito "Generic Connection Framework", implementato in un insieme di interfacce che rappresentano vari livelli di astrazione di metodologie di connessione. La decisione di non implementare direttamente, a livello di configurazione, i vari protocolli, ma di lasciare questo compito al livello applicazione (o profilo), rientra nella logica di un approccio generico al problema. In questo modo la configurazione fornisce gli "strumenti generici di base" permettendo il supporto di molti protocolli e l'utilizzo sui più disparati dispositivi (ad esempio una connessione di tipo datagram può avvenire attraverso l'IP ma anche tramite beaming su porta infrarossi).
Alla radice della gerarchia, l'interfaccia Connection, rappresenta una generica connessione che può essere aperta e chiusa attraverso i metodi open() (che in realtà non è public ma viene richiamato dal metodo statico open() presente nella - unica - classe Connector) e close().
InputConnection e OutputConnection rappresentano un dispositivo dal quale i dati possono essere rispettivamente letti o scritti, attraverso gli opportuni stream (openInputStream-openDataInputStream / openOutputStream-openDataOutputStream), StreamConnection è la combinazione di Input/Output-Connection e costituisce normalmente il punto di partenza per le classi che implementano le interfacce di comunicazione. La sua estensione ContentConnection dispone di alcuni metodi (getEncoding, getType, getLength) che consentono di ottenere informazioni sui dati trasmessi.
Come è possibile vedere nella figura 1 sono presenti altre due interfacce discendenti direttamente da Connection: StreamConnectioNotifier e DatagramConnection.
La prima rappresenta una connessione server-side di tipo socket, il metodo acceptAndOpen() crea una conessione con il client e ritorna una StreamConnection (questo tipo di connessione è simile ai server socket della Standard Edition). DatagramConnection può essere utilizzata per implementare connessioni di tipo datagram. I datagram possono essere creati(newDatagram(...)), inviati (send(Datagram datagram) ) e ricevuti (receive(Datagram datagram)). Una connessione di tipo datagram può essere aperta in modalità "server" oppure "client": nel primo caso il dispositivo resta nell'attesa di ricevere datagram nel secondo caso è lui ad iniziare la trasmissione (per aprire una connessione "datagram-server" occorre omettere il nome dell'host nella stringa di connessione di cui parleremo tra breve).



Figura 1 - Gerarchia delle interfaccie del "Generic Connection Framework".

Qualunque tipo connessione viene aperta utilizzando il metodo open() della classe Connector:

Connector.open(string_connection);

dove string_connection ha il seguente formato:

<protocol>:<address>;<parameters>

protocol indica il tipo connessione che si intende utilizzare (es. http, datagram, scrittura su file, ecc.), address permette di individuare la destinazione (può essere un indirizzo IP ma anche il nome di un file) , parameters contiene una serie di informazioni aggiuntive che possono essere funzionali per un dato tipo di connessione (ad es. baudrate per comunicazioni su porta seriale).
La figura 2 riporta alcuni esempi di stringhe di connessione, ribadiamo che nessuna di queste è implementata a livello di configurazione, utilizzando quindi un profilo che si basa su CLDC potrete trovare implementate solo alcune di queste modalità di connessione e/o altre che non compaiono nell'esempio.




Figura 2 - Esempi di stringhe di connessione


A titolo di esempio riportiamo una porzione di codice che realizza una connessione di tipo Http (utilizzando il profilo MIDP - Mobile Information Device Profile - che implementa il protocollo Http):

......
......
// La stringa di connessione con protocol-address
String connectionString = "http://193.0.0.1/servlets/MyServlet";

// Apre la connessione di tipo Http.
// Può lanciare alcune eccezioni: IllegalArgumentException
// se la stringa di connessione non è valida
// ConnectionNotFoundException nel caso la connessione non
// possa essere stabilita o il protocollo specificato non è supportato
// IOException - se si verifica un qualunque errore di I/O
HttpConnection http = (HttpConnection) Connector.open(connectionString);

//Apre gli stream di input/output sui quali è possible leggere/scrivere
InputStream is = http.openInputStream();
OutputStream os = http.openOutputStream();
...
...

//Chiude gli stream di I/O e la connessione Http
is.close();
os.close();
http.close();
...

Questa parte di codice invece stabilisce una connessione su porta seriale (utilizzando l'attuale profilo per palmari PalmOS, che si basa sulla configurazione CLDC):

// Apre una connessione su porta seriale.
// Il numero 0 individua la porta
// l'unica presente sui dispositivi PalmOS
// vengono poi settate la velocità di trasmissione e i controlli di flusso
StreamConnection serialConnection = (StreamConnection)
                                    Connector.open("
                                    comm:0;
                                    baudrate=57600;
                                    autocts=on;
                                    autorts=on");

//Apre gli stream di input/output sui quali è possible leggere/scrivere
InputStream is = serialConnection.openInputStream();
OutputStream os = serialConnection.openOutputStream();

//Chiude gli stream di I/O e la connessione seriale
is.close();
os.close();
serialConnection.close();

Come è possibile osservare si tratta di un framework molto potente e versatile, che può essere adattato alle più diverse esigenze applicative e alle caratteristiche dei vari dispositivi.

 

Il problema della sicurezza e la verifica dei file class
I meccanismi di sicurezza di Java 2 Standard Edition richiedono quantitativi di memoria troppo elevati per essere impiegati nella CLDC. Per poter mantenere comunque alto il livello di sicurezza offerto, anche su tali dispositivi, sono state introdotte alcune modifiche e anche qualche limitazione (alcune delle quali sono già emerse). Ma andiamo con ordine.
I meccanismi di verifica e di sicurezza della CLDC agiscono a due livelli distinti:

  • a livello di Virtual Machine
  • a livello applicazione

In una virtual machine standard il primo punto è espletato dal sistema runtime di Java che, tramite il verificatore (dei file class) si preoccupa di stabilire se ciò che è contenuto all'interno dei file class é bytecode java valido oppure no. In particolare il verificatore controlla che il bytecode non contenga riferimenti a locazioni di memoria non valide o comunque al di fuori della memoria riservata dalla macchina virtuale agli oggetti java, che non vengano effettuate conversioni illegali di tipo, che i parametri dei metodi siano appropriati e che le istruzioni alle quali vengono applicati siano correte, che le operazioni dello stack non producano situazioni di overflow, che le operazioni con i registri non portino a condizioni di errore. Tutto ciò doveva essere garantito e mantenuto anche nella CLDC della J2ME. E così è stato seppure con alcune modifiche e accorgimenti.
Il processo di verifica standard di java utilizza algoritmi troppo complessi che richiedono sia ingenti quantitativi di memoria sia elevate capacità di calcolo (perlomeno paragonandole a quelle dei dispositivi ai quali CLDC si rivolge - cellulari, palmari, smart-phone) basti pensare che il verificatore di una virtual machine stanadard occupa 50 Kbyte e richiede dai 30 ai 100 Kbyte di memoria a runtime.
Quantitativi troppo elevati per dispositivi che possono avere memoria dinamica di 32 Kbyte.
Per superare questo problema è stata introdotta nella CLDC una fase supplementare: la pre-verifica.
In pratica il processo di verifica avviene in due fasi distinte:

  • al di fuori del dispositivo (off-device pre-verification)
  • nel dispositivo (in-device verification);

Il primo passo (pre-verifica) avviene sulla macchina di sviluppo o sul server dalla quale viene scaricata l'applicazione e, viene eseguito da un tool di pre-verifica che, si occupa di inserire nei file class speciali attributi, chiamati Stack Map, che permettono al sistema runtime di effettuare la verifica con prestazioni molto superiori (rispetto al sistema di verifica standard). Inoltre, la verifica con il meccanismo dello Stack Map riduce notevolmente la complessità degli algoritmi di controllo la cui dimensione richiede circa 100 byte (byte!) di memoria a runtime, questo perché l'inserimento di tali attributi consente una scansione lineare del bytecode senza processi "ricorsivi". Una volta superata la fase di pre-verifica, l'applicazione java può essere installata sul dispositivo finale, dove al momento dell'esecuzione (runtime) subirà come abbiamo detto un secondo controllo (la verifica). La figura 3 mostra come avviene il processo di verifica. Come si può notare il pre-verificatore accetta in input il file class generato dalla compilazione e produce anch'esso un file class. Quest'ultimo contiene gli attributi aggiuntivi degli Stack Map, infatti dopo la pre-verifica le dimensioni dei file class risultano aumentate di circa il 5 % rispetto alla dimensione originale.


Figura 3 - Il processo di verifica

E' importante sottolineare che un file class sottoposto a pre-verifica continua ad essere eseguibile anche in una macchina virtuale standard, la quale semplicemente ignora gli attributi dello Stack Map, il bytecode rimane quindi compatibile con le altre versioni di Java (Standard ed Enterprise Edition).

Le operazioni di pre-verifica e verifica di fatto assicurano la validità di un'applicazione Java cioè la sua conformità alle regole del linguaggio. Questo però, non è di per se sufficiente a garantire un elevato grado sicurezza. Ad esempio la verifica non si occupa di gestire l'accesso alle risorse del sistema (file-system, porte di comunicazione, reti, ecc.). Nella Standard Edition, l'accesso alle risorse esterne è controllato nella attraverso il concetto di "security manager", che viene richiamato ogniqualvolta un'applicazione o il sistema runtime stesso richiede l'accesso ad una risorsa protetta.
A causa delle risorse che richiede, questo approccio però risulta poco adatto alla CLDC, è stato pensato quindi un semplice modello "Sendbox" che rappresentasse un compromesso tra sicurezza e funzionalità. Secondo le regole imposte da tale modello ogni applicazione Java si trova ad essere eseguita in un ambiente chiuso e ben circoscritto, in particolare:

  • ogni applicazione deve aver super il processo di verifica;
  • ogni applicazione a accesso ad un ben determinato set di API (quelle previste dalla CLDC, dal profilo utilizzato e eventuali altri classi);
  • non è prevista la possibilità di ridefinire caricatori di classe a livello applicazione. L'unico class loader ammesso è quello standard della virtual machine;
  • il codice nativo non è accessibile alle applicazioni, quindi non è ammesso codice che utilizzi metodi nativi o che in qualche modo acceda a funzioni native che non rientrano in quelle previste dalla CLDC o dal profilo utilizzato;

Nei prossimi numeri, parleremo dei profili PDAP e MIDP che si basano sulla configurazione CLDC, avremo quindi modo di vedere l'utilizzo pratico delle librerie di cui abbiamo parlato in questo articolo. Con l'ausilio di semplici esempi mostreremo come creare applicazioni reali partendo dai file java fino ad arrivare all'eseguibile vero e proprio (in pratica il "percorso" di figura 3).

Appendice: classi ed interfacce CLDC

Classi di sistema

java.lang.Object
java.lang.Class
java.lang.Runtime
java.lang.System
java.lang.Thread
java.lang.Runnable (interfaccia)
java.lang.String
java.lang.StringBuffer
java.lang.Throwable

Classi di tipi di dati:

java.lang.Boolean
java.lang.Byte
java.lang.Short
java.lang.Integer
java.lang.Long
java.lang.Character

Classi di utilità (collezioni di oggetti)
java.util.Enumeration (interfaccia)
java.util.Vector
java.util.Stack
java.util.Hashtable

Classi di utilità (manipolazione di data e ora)
java.util.Calendar
java.util.Date
java.util.TimeZone

Altre classi di utilità
java.util.Random
java.util.Math

Classi di gestione dell'I/O
java.io.InputStream
java.io.OutputStream
java.io.ByteArrayInputStream
java.io.ByteArrayOutputStream
java.io.DataInput
(interfaccia)
java.io.DataOutput (interfaccia)
java.io.DataInputStream
java.io.DataOutputStream
java.io.Reader
java.io.Writer
java.io.InputStreamReader
java.io.OutputStreamReader
java.io.PrintStream

Classi del Generic Connection Framework
javax.microedition.io.Connection (interfaccia)
javax.microedition.io.ContentConnection (interfaccia)
javax.microedition.io.Datagram (interfaccia)
javax.microedition.io.DatagramConnection (interfaccia)
javax.microedition.io.InputConnection (interfaccia)
javax.microedition.io.OutputConnection (interfaccia)
javax.microedition.io.StreamConnection (interfaccia)
javax.microedition.io.StreamConnectionNotifier (interfaccia)
javax.microedition.io.Connector

Classi relative ad eccezioni ed errori
java.lang.Exception
java.lang.ClassNotFoundException
java.lang.IllegalAccessException
java.lang.InstantiationException
java.lang.InterruptedException
java.lang.RuntimeException
java.lang.ArithmeticException
java.lang.ArrayStoreException
java.lang.ClassCastException
java.lang.IllegalArgumentException
java.lang.IllegalThreadStateException
java.lang.NumberFormatException
java.lang.IllegalMonitorStateException
java.lang.IndexOutOfBoundsException
java.lang.ArrayIndexOutOfBoundsException
java.lang.NullPointerException
java.lang.SecurityException

java.util.EmptyStackException
java.util.NoSuchElementException

java.io.EOFException
java.io.IOException
java.io.InterruptedIOException
java.io.UnsupportedEncodingException
java.io.UTFDataFormatException

javax.microedition.io.ConnectionNotFoundException

java.lang.Error
java.lang.VirtualMachineError
java.lang.OutOfMemoryError


Riferimenti
http://www.embedded.oti.com IBM J9 VM
http://home.rochester.rr.com/ohommes/MathFP/index.html MathFP
http://java.sun.com/j2me/docs/zip/cldcapi.zip Il link per scaricare la documentazione realtiva alle API CLDC;
http://java.sun.com/aboutJava/communityprocess/final/jsr030/index.html Contiene le specifiche CLDC;
http://java.sun.com/products/cldc/ Sito dal quale è possibile effettuare il download della CLDC. Contiene alcuni interessanti link a FAQ, mailing-list e documentazione varia.

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