In questo articolo vedremo passo per passo come generare codice Java da un diagramma UML. Per mettere in pratica il nostro intento utilizzeremo gli strumenti di Eclipse per il disegno di diagrammi UML e le API per la loro manipolazione.
Setup
Provvediamo anzitutto all'installazione dei plugin necessari. Selezioniamo dal menu Help -> Install new software
e, nella finestra che appare scegliamo di lavorare con tutti gli update site
.
Quando viene caricata la lista dei plugin disponibili espandiamo Modeling
e selezioniamo UML 2 Extender SDK
e UML 2 Tools SDK
. Procediamo con l'installazione e riavviamo Eclipse, come suggerito.
Il primo diagramma
Iniziamo disegnando un diagramma. Creiamo un nuovo progetto e chiamiamolo MyUmlDiagrams
. Nel nostro progetto creiamo un Class Diagram:
New -> Other -> UML 2.1 Diagrams -> Class Diagram
Come nome manteniamo MyFirstModel
. Ora possiamo iniziare a disegnare il nostro diagramma in maniera visuale.
Disegnare diagrammi con questo strumento dovrebbe risultare abbastanza intuitivo, ma chi avesse dei dubbi può consultare questo tutorial realizzato da uno degli sviluppatori.
È disponibile in allegato un semplice esempio, in cui troviamo rappresentata una classe Site
, collegata a degli Article
. Ogni Article
avrà un Author
ed ogni Author
potrà avere da zero a molti Article
. Author
è una specificazione di Person
. Aggiungiamo poi alcuni attributi per ogni classe, il risultato ottenuto è riportato in figura.
Includere le librerie UML
Creiamo ora un Java Project e chiamiamolo "BeanGenerator". In questo progetto dobbiamo includere le librerie necessarie alla manipolazione di diagrammi UML: si tratta di componenti creati dal progetto Eclipse disponibili sotto la cartella plugins
dell'installazione del nostro IDE.
Per includere nella nostra applicazione standalone questi plugin, concepiti per l'utilizzo interno ad Eclipse. Li importiamo nella definizione del nostro progetto, grazie alla tab Libraries
della maschera Java Settings
.
Ci serviranno infatti alcuni JAR specifici per le API UML e alcuni altri relativi ad EMF; questo perché le API UML vengono implementate facendo riferimento a questa tecnologia, EMF, attorno alla quale si sta sviluppando molto fermento e che è alla base di diversi progetti interessanti (i più curiosi possono dare un'occhiata a XText).
Per importare i JAR, iniziamo creando una cartella lib
all'interno del nostro progetto, apriamo poi la cartella plugin
, che si trova nella directory di installazione di Eclipse e, da qui, copiamo nella nostra cartella lib
tutti gli archivi JAR che hanno i seguenti prefissi:
org.eclipse.emf.common org.eclipse.emf.ecore org.eclipse.emf.ecore.xmi org.eclipse.uml2 org.eclipse.uml2.common org.eclipse.uml2.uml
Possiamo semplificare la cosa filtrando i file con lo strumento di ricerca del sistema operativo.
Fatto ciò non ci resta che inserire le librerie nel Build Path. Clicchiamo col tasto destro sul nome del progetto e poi selezioniamo Build Path -> Configure Build Path
. Clicchiamo su Add External JARs...
, poi possiamo selezionare i JAR dalla cartella lib
.
Una volta finito troveremo le librerie tra quelle referenziate.
Caricare un diagramma UML
Per prima cosa realizziamo un semplice programma di esempio per sperimentare le API UML e caricare il modello che abbiamo realizzato precedentemente. Creiamo la classe ModelLoader
e nel corpo del main
effettuiamo per prima cosa la "registrazione" delle risorse che definiscono UML nel mondo EMF:
ResourceSet resourceSet = new ResourceSetImpl();
resourceSet.getPackageRegistry().put(UMLPackage.eNS_URI, UMLPackage.eINSTANCE);
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(UMLResource.FILE_EXTENSION, UMLResource.Factory.INSTANCE);
Map<URI,URI> uriMap = resourceSet.getURIConverter().getURIMap();
URI uri = URI.createURI("jar:file:/"+UML_RESOURCES_JAR+"!/");
uriMap.put(URI.createURI(UMLResource.LIBRARIES_PATHMAP), uri.appendSegment("libraries").appendSegment(""));
uriMap.put(URI.createURI(UMLResource.METAMODELS_PATHMAP), uri.appendSegment("metamodels").appendSegment(""));
uriMap.put(URI.createURI(UMLResource.PROFILES_PATHMAP), uri.appendSegment("profiles").appendSegment(""));
La variabile UML_RESOURCE_JAR
è definita come statica e fa riferimento ad un jar disponibile sotto la cartella plugins da cui abbiamo già estratto diversi componenti. Questo jar si chiama org.eclipse.uml2.uml.resources
e a differenza degli altri non va incluso nel build path ma viene caricato dinamicamente dall'applicazione.
Entriamo finalmente nel vivo. Nel seguito del main andiamo a definire l'URI del modello da caricare:
URI modelUri = URI.createURI("file:/"+MODEL_FILE);
Nell'esempio abbiamo definito il path del file come una variabile statica, nulla impedisce però di ottenere questa informazione dalla linea di comando come parametro dell'applicazione.
Carichiamo poi la risorsa contenuta nel nostro file:
Resource resource = resourceSet.getResource(modelUri, true);
Da questa risorsa provvediamo ad estrarre l'intero Package che contiene tutti gli elementi del nostro diagramma. Qui è necessario un cast perché il metodo di estrazione è necessariamente generico e ritorna un semplice Object
:
Package pkg = (Package)EcoreUtil.getObjectByType(resource.getContents(), eclass);
Verifichiamo che il modello sia stato caricato e nel caso l'operazione sia andata a buon fine stampiamo il nome del nostro modello:
if (null == pkg) {
throw new RuntimeException("Not loaded from uri '" + modelUri + "' as " + eclass);
} else {
System.out.println("Package loaded: "+pkg.getName());
}
Generazione di codice
Creiamo ora un'applicazione che legga il nostro modello e generi in un'apposita cartella dei file .java
contenenti i Java Bean corrispondenti alle classi presenti nel modello. Per fare questo dobbiamo gestire le proprietà, l'ereditarietà e le relazioni fra classi.
Nell'esempio allegato abbiamo racchiuso tutte le chiamate che stampano il codice nella classe JavaCodePrinter
. Nel caso si volesse generare del codice più complesso di quello che vedremo nel proseguimento dell'articolo è bene fare ricorso a qualche motore di template (ad esempio Apache Velocity).
Dopo queste premesse iniziamo creando la classe BeanGenerator
ed eseguendo il refactoring del codice di inizializzazione e di caricamento del diagramma UML dell'esempio precedente (vedete il codice allegato per i dettagli). La prima operazione che eseguiamo è la cancellazione di tutto il contenuto della cartella in cui andremo ad inserire il codice generato:
instance.cleanup();
Ora eseguiamo un loop sulle classi presenti nel modello ed invochiamo per ognuna di queste il metodo generateClass che provvederà alla generazione vera e propria:
for (Object clazzAsObj : EcoreUtil.getObjectsByType(pkg.getOwnedMembers(), UMLPackage.Literals.CLASS))
{
Class clazz = (Class)clazzAsObj;
try {
instance.generateClass(clazz);
} catch (IOException e) {
e.printStackTrace();
}
}
Dichiarazione della classe
Esaminiamo il metodo generateClass
che contiene la parte più interessante del codice. Si inizia assicurandoci che la directory relativa al package contenente la classe esista:
pkgDir.mkdirs();
Instanziamo il file che conterrà il codice sorgente:
File file = new File(pkgDir.getPath()+File.separator+clazz.getName()+".java");
Dopo aver inizializzato un'istanza di JavaCodePrinter
(classe di cui abbiamo discusso in precedenza) iniziamo dichiarando il package:
p.packageDef(pkgName);
La dichirazione della classe Java comincia con la parola chiave public
:
p.modPublic();
proseguiamo con la parola chiave class
seguita dal nome della classe:
p.classDef(clazz.getName());
Generalizzazioni
A questo punto vogliamo dichiarare eventuali superclassi. Esaminiamo le generalizzazioni di cui la nostra classe UML fa parte. Se c'è n'è più di una non sarà possibile rappresentare il modello in Java (per lo meno non senza complicare significativamente il codice generato, cosa che qui intendiamo evitare) per cui in questo caso ci limitiamo a stampare un messaggio di errore. Se invece c'è una sola generalizzazione la nostra classe Java estenderà la classe Java corrispondente alla classe UML all'altro capo della generalizzazione:
p.extendsDecl(clazz.getGeneralizations().get(0).getGeneral().getName());
Attributi
All'interno della classe Java inseriamo un campo ed i relativi getter e setter per ogni attributo della classe UML:
for (Property property : clazz.getAttributes())
{
String javaType = umlTypeToJavaType(property.getType());
if (javaType!=null)
{
p.field(property.getName(),javaType);
p.getter(property.getName(),javaType);
p.setter(property.getName(),javaType);
} else {
System.out.println("Class "+clazz.getName()+": Skipping property "+property.getName()+" of type "+property.getType().getName()+" because I don't know to which Java type map it");
}
}
Se gli attributi fanno riferimento a tipi UML di cui non conosciamo il corrispettivo tipo Java stampiamo un messaggio informativo e ignoriamo l'attributo in questione.
Associazioni
A questo punto vediamo come si gestiscono le associazioni fra classi. Ogni associazione UML ha due estremità, ognuna delle quali fa riferimento ad una classe UML. Per prima cosa individuiamo l'indice dell'estremità cui si trova la classe UML corrente nell'associazione in esame. Fatto questo possiamo facilmente calcolare quale sia l'indice dell'altra classe UML coinvolta nell'associazione così da individuarne il nome ed il ruolo che assume nell'associazione:
String otherEndName = association.getMemberEnds().get(otherIndex).getName();
String otherTypeName = association.getEndTypes().get(otherIndex).getName();
Possiamo utilizzare il nome della classe UML come tipo Java perché per ogni classe UML abbiamo generato una corrispondente classe Java nello stesso package.
Ora esaminiamo la molteplicità: se il numero di massimo di elementi per quell'associazione è diverso da uno utilizziamo una lista per rappresentarla altrimenti un semplice campo:
int upper = association.getMemberEnds().get(otherIndex).getUpper();
if (upper==1)
{
p.field(otherEndName,otherTypeName);
p.getter(otherEndName,otherTypeName);
p.setter(otherEndName,otherTypeName);
} else {
p.fieldList(otherEndName,otherTypeName);
p.getterList(otherEndName,otherTypeName);
p.adder(otherEndName,otherTypeName);
}
Conclusioni
Ora eseguendo BeanGenerator otterremo la generazione di quattro classi Java nella cartella src-gen
. Modificando il modello e rieseguendo la generazione avremo del codice sempre allineato con il diagramma UML.