Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Generare classi Java dal modello UML con Eclipse

Generare automaticamente le classi Java e le relazioni a partire da un modello UML
Generare automaticamente le classi Java e le relazioni a partire da un modello UML
Link copiato negli appunti

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.

Figura 1. Selezionare i tool per UML
Selezionare i tool per UML

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.

Figura 2. Esempio di class diagram
Esempio di class diagram

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.

Figura 3. Copiare le librerie da Eclipse
Copiare le librerie da Eclipse

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.

Figura 4. Referenziare le librerie
Referenziare le librerie

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.

Ti consigliamo anche