In questa quarta e ultima parte della serie dedicata all'esplorazione e visualizzazione di ontologie tramite Google Web Toolkit, affronteremo la parte più interessante del progetto: la creazione di un widget semantico riutilizzabile per la visualizzazione di tassonomie OWL.

Nelle parti precedenti abbiamo visto come configurare l'ambiente GWT, come utilizzare le API di JENA per l'accesso alle ontologie e come strutturare l'architettura client-server. Ora metteremo tutto insieme creando il TaxonomyTree, un componente grafico che permette di navigare interattivamente le gerarchie di classi presenti in un'ontologia.

L'obiettivo è realizzare un widget completamente riutilizzabile che, data una tassonomia in formato DTO, sia in grado di generare un albero navigabile mostrando label, commenti e permettendo all'applicazione di gestire gli eventi in modo personalizzato.

I Data Transfer Object

Prima di procedere con l'implementazione del servizio e del widget, è necessario definire le strutture dati che verranno scambiate tra client e server. Creeremo due DTO: TaxonomyDTO per la tassonomia completa e TaxonomyItemDTO per i singoli elementi.

Di seguito il codice della classe TaxonomyItemDTO che rappresenta un singolo nodo della tassonomia:

public class TaxonomyItemDTO implements IsSerializable {
    private String id;
    private String label;
    private String comment;
    private String nodeType;
    private Collection<TaxonomyItemDTO> subItems = new ArrayList<TaxonomyItemDTO>();
    public static final String CLASS_TYPE_NAME = "class";
    public static final String INSTANCE_TYPE_NAME = "instance";

    public TaxonomyItemDTO(String id, String label, String comment,
            String nodeType) {
        super();
        this.id = id;
        this.label = label;
        this.comment = comment;
        this.nodeType = nodeType;
    }
    public TaxonomyItemDTO() {
        super();
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getLabel() {
        return label;
    }
    public void setLabel(String label) {
        this.label = label;
    }
    public String getComment() {
        return comment;
    }
    public void setComment(String comment) {
        this.comment = comment;
    }
    public String getNodeType() {
        return nodeType;
    }
    public void setNodeType(String nodeType) {
        this.nodeType = nodeType;
    }
    public Collection<TaxonomyItemDTO> getSubItems() {
        return subItems;
    }
    public void setSubItems(Collection<TaxonomyItemDTO> subItems) {
        this.subItems = subItems;
    }
    public void addSubItem(TaxonomyItemDTO subItem) {
        this.subItems.add(subItem);
    }
}

Definizione dell'Ontology Service

A questo punto possiamo definire l'Ontology Service. Come si è detto il servizio dovrà eseguire due operazioni per il client:

caricare l'ontologia sul server (loadOntology) restituire la tassonomia (getCompleteTaxonomy)

In base a questi requisiti possiamo immediatamente definire la sua interfaccia sincrona. In questo modo potremo far fare tutto il lavoro di generazione della asincrona e relativo aggiornamento al plug-in di eclipse. Di seguito riporto l'interfaccia OntologyService.java:

package org.example.gwt.semantic.client;
import org.example.gwt.semantic.client.dto.TaxonomyDTO;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
@RemoteServiceRelativePath("ontoservice")
public interface OntologyService extends RemoteService {

    // Carica l'ontologia RDF/OWL sul server
    public String loadOntology(String url);

    // Ritorna la tassonomia completa.
    public TaxonomyDTO getCompleteTaxonomy();
}

A partire da questa genero l'interfaccia asincrona usando il plug-in:

package org.example.gwt.semantic.client;
import org.example.gwt.semantic.client.dto.TaxonomyDTO;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface OntologyServiceAsync {
    void getCompleteTaxonomy(AsyncCallback<TaxonomyDTO> callback);
    void loadOntology(String url, AsyncCallback<String> callback);
}

Finalmente è arrivato il momento di scrivere un po' di logica e quindi di attivare qualche neurone in più: nel package server creo quindi la classe di implementazione del servizio. Di seguito riporto il codice della classe OntologyServiceImpl.java:

import org.example.gwt.semantic.client.OntologyService;
import org.example.gwt.semantic.client.dto.TaxonomyDTO;
import org.example.gwt.semantic.client.dto.TaxonomyItemDTO;
import java.util.Iterator;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.hp.hpl.jena.*;

public class OntologyServiceImpl extends RemoteServiceServlet implements
        OntologyService {

    private static String LOAD_SUCCESS_MSG = "Ontology loaded succesfully";
    private static String LOAD_FAILURE_MSG = "ERROR ON LOADING ONTOLOGY: ";
    private OntModel ontModel = null;
    public TaxonomyDTO getCompleteTaxonomy() {
        TaxonomyDTO taxonomyDTO = new TaxonomyDTO();
        taxonomyDTO.setRoot(generateRootItem());
        return taxonomyDTO;
    }
    private TaxonomyItemDTO generateRootItem() {
        if (ontModel==null) return null;
        else {
            Iterator<OntClass> i = ontModel.listHierarchyRootClasses()
                .filterDrop( new Filter<OntClass>() {
                    public boolean accept( OntClass r ) {
                        return r.isAnon();
                    }} );
            TaxonomyItemDTO rootItem = new TaxonomyItemDTO();
            rootItem.setId("owl:Thing");
            rootItem.setLabel("owl:Thing");
            rootItem.setComment("OWL Ontology root class");
            while (i.hasNext()) {
                rootItem.addSubItem(generateSubItemTreeFromClass(i.next()));
            }
            return rootItem;
        }
    }
    private TaxonomyItemDTO generateSubItemTreeFromClass(OntClass ontClass) {
        Iterator<OntClass> ite = ontClass.listSubClasses()
            .filterDrop( new Filter<OntClass>() {
                public boolean accept( OntClass r ) {
                    return r.isAnon();
                }} );

        TaxonomyItemDTO item
            = new TaxonomyItemDTO(ontClass.getLocalName(),
                ontClass.getLabel(null), ontClass.getComment(null),
                TaxonomyItemDTO.CLASS_TYPE_NAME);
        while (ite.hasNext()) {
            OntClass subClass = (OntClass) ite.next();
            item.addSubItem(generateSubItemTreeFromClass(subClass));
        }
        return item;
    }
    public String loadOntology(String url) {

        this.ontModel = ModelFactory.createOntologyModel( OntModelSpec.OWL_MEM, null );

        try {
            ontModel.read(url);
        } catch (Exception e) {
            return LOAD_FAILURE_MSG.concat(e.getMessage());
        }

        return LOAD_SUCCESS_MSG;
    }
}

Dal codice si può notare come il servizio faccia pesantemente uso dell'OntModel di JENA per caricare l'ontologia e per navigare la gerarchia delle classi. In particolare il metodo pubblico getCompleteTaxonomy() non fa altro che chiamare ricorsivamente il metodo generateSubTreeFromClass per creare il TaxonomyTree da ritornare poi al client che lo ha chiamato. Per fare questo sono stati usati in particolare il metdo listHierarchyRootClasses() dell'OntModel che ritorna un iteratore su tutte le classi di primo livello della tassonomia (quelle sotto owl:thing per intenderci). Per ogni OntClass viene lanciato il metodo privato ricorsivo generateSubItemTreeFromClass(OntClass) che ritorna un TaxonomyTreeItem relativo con tutti i suoi figli generati ricorsivamente. Non c'è molto altro da dire a riguardo se non il fatto che tutti gli iteratori sulle sottoclassi sono sempre filtrati in modo da scartare le classi anonime (come per esempio le restrizioni owl):

Iterator<OntClass> ite = ontClass.listSubClasses()
    .filterDrop( new Filter<OntClass>() {
        public boolean accept( OntClass r ) {
            return r.isAnon();
        }} );

Prima di passare alla parte client resta da aggiungere il servizio appena creato nel web.xml dell'applicazione (maledetto plugin...questo potevi farlo anche tu però!). Di seguito riporto il mio file war/WEB-INF/web.xml per chiarezza.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- Servlets -->
<servlet>
<servlet-name>ontologyServlet</servlet-name>
<servlet-class>org.example.gwt.semantic.server
.OntologyServiceImpl</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>ontologyServlet</servlet-name>
<url-pattern>/taxonomytree/ontoservice</url-pattern>
</servlet-mapping>
<!-- Default page to serve -->
<welcome-file-list>
<welcome-file>TaxonomyTreeTest.html</welcome-file>
</welcome-file-list>
</web-app>

Un widget semantico: il TaxonomyTree

Ora possiamo creare il nostro widget semantico riutilizzabile. Per far questo useremo il metodo classico di customizzazione dei widget secondo GWT, cioè creeremo un Composite, ossia un widget composto appunto di altri widget con una sua logica interna specifica.

Prima di entrare nel merito del codice vorrei soffermarmi su alcuni punti di interesse riguardo la creazione di widget riutilizzabili:

è pur sempre JavaScript: il widget è un componente grafico che verrà trasformato in JavaScript e come tale non può lavorare direttamente con le librerie esterne ma solo usando librerie GWT e classi serializzabili (vedi TaxonomyDTO); il widget NON dialoga col server GWT: il widget non dovrebbe in linea di massima accedere direttamente ad un apposito servizio GWT per evitare di vincolare il componente e renderlo il più riutilizzabile possibile (e far scalare meglio l'intera architettura); massima customizzabilità: il widget dovrebbe implementare solo ed esclusivamente la logica di visualizzazione base e lasciare la totale libertà all'applicazione di gestire gli handler degli eventi, i command e gli stili.

Bene, ci siamo dati un bel codice di autoregolamentazione in materia di creazione di widget custom, ora passiamo al nostro TaxonomyTree.

L'idea che sta alla base di un widget semantico è quella di poter disporre di un componente grafico che, date delle informazioni provenienti da una ontologia, sia in grado di gestirle e visualizzarle. Nel nostro caso vogliamo creare un widget che:

data una tassonomia OWL [OWL] in formato TaxonomyDTO sia in grado di generare un albero navigabile delle classi; ogni classe nell'albero sarà visualizzata preferibilmente tramite rdf:label se esiste, altrimenti tramite rdf:id; al passaggio del mouse su una classe dell'albero, se disponibile, dovrà essere mostrato come tooltip lo rdf:comment della classe a titolo descrittivo; il widget non gestirà il click del mouse sulle classi, lasciando la possibilità all'applicazione esterna di settare l'handler del click per poter applicare la sua logica applicativa.

TaxonomyTree in dettaglio
TaxonomyTree in dettaglio

Di seguito riporto il codice della classe TaxonomyTree:

public class TaxonomyTree extends Composite {
    private TaxonomyDTO taxonomyDTO = null;
    private Tree taxonomyTree = new Tree();
    private ClickHandler taxonomyItemClickHandler = null;
    private MouseOverHandler taxonomyItemMouseOverHandler = null;
    public ClickHandler getTaxonomyItemClickHandler() {
        return taxonomyItemClickHandler;
    }
    public void setTaxonomyItemClickHandler(ClickHandler taxonomyItemClickHandler) {
        if (taxonomyItemClickHandler!=null) this.taxonomyItemClickHandler = taxonomyItemClickHandler;
    }
    public MouseOverHandler getTaxonomyItemMouseOverHandler() {
        return taxonomyItemMouseOverHandler;
    }
    public void setTaxonomyItemMouseOverHandler(
            MouseOverHandler taxonomyItemOnMouseOverHandler) {
        if (taxonomyItemClickHandler!=null) this.taxonomyItemMouseOverHandler = taxonomyItemOnMouseOverHandler;
    }
    private void init() {
        // Creo il pannello verticale che farà da contenitore
        VerticalPanel panel = new VerticalPanel();
        panel.add(taxonomyTree);
        // Chiamo init.Widget() per inizializzare il composite
        initWidget(panel);
        // Assegno uno stile css al composite
        setStyleName("TaxonomyTreeStyle");

    }
    public TaxonomyTree() {
        init();
    }
    public TaxonomyTree(TaxonomyDTO taxonomyDTO) {
        init();
        this.taxonomyDTO = taxonomyDTO;
    }
    public TaxonomyDTO getTaxonomyDTO() {
        return taxonomyDTO;
    }
    public void setTaxonomyDTO(TaxonomyDTO taxonomyDTO) {
        this.taxonomyDTO = taxonomyDTO;
    }

    public void generateTree() {
        if (taxonomyDTO!=null) {

            // Resetto l'albero
            taxonomyTree.clear();

            TaxonomyItemDTO root = taxonomyDTO.getRoot();

            // Lancio la generazione ricorsiva a partire dalla root
            TreeItem rootItem = generateSubTree(root);

            // Aggiungo l'item root all'albero
            taxonomyTree.addItem(rootItem);
        }
    }
    private TreeItem generateSubTree(TaxonomyItemDTO taxonomyItemDTO) {

        Label itemLabel = new Label();

        // Se esiste una rdf:label uso quella altrimenti uso rdf:id
        if (taxonomyItemDTO.getLabel()!=null
                && !taxonomyItemDTO.getLabel().isEmpty()) itemLabel.setText(
                taxonomyItemDTO.getLabel());
        else itemLabel.setText(taxonomyItemDTO.getId());

        // Setto gli handler di click e mouse over passati dall'esterno.
        if (taxonomyItemClickHandler!=null) itemLabel.addClickHandler(
                taxonomyItemClickHandler);
        if (taxonomyItemMouseOverHandler!=null) itemLabel.addMouseOverHandler(
                taxonomyItemMouseOverHandler);
        itemLabel.setTitle(taxonomyItemDTO.getComment());
        TreeItem treeItem = new TreeItem(itemLabel);
        Iterator<TaxonomyItemDTO> ite = taxonomyItemDTO.getSubItems().iterator();
        while (ite.hasNext()) {
            TaxonomyItemDTO taxonomyChildItemDTO = (TaxonomyItemDTO) ite.next();
            treeItem.addItem(generateSubTree(taxonomyChildItemDTO));
        }
        return treeItem;
    }
    public TreeItem getSelectedTaxonomyItem() {
        return taxonomyTree.getSelectedItem();
    }
}

In pratica il TaxonomyTree non è altro che un Composite che contiene un normalissimo GWT Tree capace di generarsi a partire dal TaxonomyDTO passato tramite costruttore (o settatoin un secondo momento). La generazione vera e propria avviene quando si lancia il metodo pubblico generateTree() il quale in base al TaxonomyDTO memorizzato pulisce il vecchio albero (se necessario) e ne genera ricorsivamente un altro. Ogni TreeItem del TaxonomyTree viene generato ricorsivamente dal metodo privato generateSubTree() che aggiunge all'item oltre che i figli anche una Label che visualizza rdf:id o rdf:label della classe, setta il tooltip con rdf:comment e infine associa, se non nulli, gli handler degli eventi Click e MouseOver passati dall'esterno.

Assembliamo il tutto nell'EntryPoint: TaxonomyTreeTest

Ora abbiamo tutto quello che ci serve per creare l'EntryPoint della nostra applicazione di test. Di seguito riporto un sunto (per questioni di spazio) delle parti più importanti del codice della classe entryPoint TaxonomyTreeTest.java (attenzione qua il "copia incolla" non è compilabile!!!):

public class TaxonomyTreeTest implements EntryPoint {
    private static final String SERVER_ERROR = "An error occurred while "
            + "attempting to contact the server. Please check your network "
            + "connection and try again.";
    /**
    - Create a remote service proxy to talk to the server-side Greeting service.
    */
    private final OntologyServiceAsync ontologyService = GWT.create(OntologyService.class);
    public void onModuleLoad() {
        final TaxonomyTree taxonomyTree = new TaxonomyTree();
        final Button loadButton = new Button("Load");
        final Label stateMsg = new Label("Nessuna ontologia caricata");
        final DialogBox dbox = new DialogBox(true);
        final ListBox urlListBox = new ListBox();
        final Label selectOntologyLabel
                = new Label("Select wich ontology to load from the server: ");
        selectOntologyLabel.setStyleName("selectLabel");
        dbox.setText("CLASS DETAILS");
        dbox.setAnimationEnabled(true);
        // Carico la lista degli URL locali delle ontologie
        urlListBox.setTitle("List of ontologies on the servers...");
        urlListBox.addItem("MokaByte", "file:C:/Users/Matteo/workspace-
                GWT/TaxonomyTreeTest/war/ontologies/MokaByte.owl");
        final HorizontalPanel loadPanel = new HorizontalPanel();
        final VerticalPanel mainPanel = new VerticalPanel();
        loadPanel.add(urlListBox);
        loadPanel.add(loadButton);
        // We can add style names to widgets
        loadButton.getElement().setId("loadButton");
        mainPanel.add(selectOntologyLabel);
        mainPanel.add(loadPanel);
        mainPanel.add(stateMsg);
        mainPanel.add(taxonomyTree);
        ...
        RootPanel.get("mainBox").add(mainPanel);
        /**
        - Gestore eventi da passare al TaxonomyTree
        */
        class TaxonomyTreeItemHandler implements ClickHandler,
                MouseOverHandler {
            @Override
            public void onClick(ClickEvent event) {
                Label itemLabel = (Label)taxonomyTree.getSelectedTaxonomyItem().getWidget();
                VerticalPanel vpanel = new VerticalPanel();
                HTML nameLabel = new HTML("<b>Class name: </b>"+itemLabel.getText());
                HTML descriptionLabel
                        = new HTML("<b>Class descritpion: </b>"+itemLabel.getTitle());
                Button closeButton = new Button("Close");
                closeButton.getElement().setId("closeButton");
                closeButton.addClickHandler(new ClickHandler() {
                    public void onClick(ClickEvent event) {
                        dbox.hide();
                        dbox.clear();
                    }
                });
                vpanel.add(nameLabel);
                vpanel.add(descriptionLabel);
                vpanel.add(closeButton);
                dbox.add(vpanel);
                dbox.center();
            }

            @Override
            public void onMouseOver(MouseOverEvent event) {
                // TODO Auto-generated method stub
            }
        }

        /**
        - Gestore eventi per loadButton, comboBox e altro
        */
        class MyHandler implements ClickHandler {
            /**
            - Gestione Click sul loadButton
            */
            public void onClick(ClickEvent event) {
                loadOntology();
            }
            /**
            - Carica l'ontologia sul server
            */
            private void loadOntology() {
                ontologyService.loadOntology(urlListBox.getValue(urlListBox.getSelectedIndex()),
                        new AsyncCallback<String>() {
                            public void onFailure(Throwable caught) {
                                RootPanel.get("mainBox").add(new HTML(caught.getMessage()));
                            }
                            public void onSuccess(String result) {
                                stateMsg.setText("Ontologia caricata con
                                        successo. Ora carico la tassonomia...");
                                loadTaxonomyTree();
                            }
                        });
            }
            /**
            - Carica il TaxonomyTree con la tassonomia restituita dal server
            */
            private void loadTaxonomyTree() {
                ontologyService.getCompleteTaxonomy(
                        new AsyncCallback<TaxonomyDTO>() {
                            public void onFailure(Throwable caught) {
                                stateMsg.setText(caught.getMessage());
                            }
                            public void onSuccess(TaxonomyDTO taxonomyDTO) {
                                taxonomyTree.setTaxonomyDTO(taxonomyDTO);
                                taxonomyTree.generateTree();
                                stateMsg.setText("Tassonomia caricata con successo");
                            }
                        });
            }
        }

        // Setto gli hanlder
        TaxonomyTreeItemHandler itemHandler = new TaxonomyTreeItemHandler();
        taxonomyTree.setTaxonomyItemClickHandler(itemHandler);
        taxonomyTree.setTaxonomyItemMouseOverHandler(itemHandler);
        MyHandler handler = new MyHandler();
        loadButton.addClickHandler(handler);
        urlListBox.addChangeHandler(handler);
    }
}

L'entrypoint al suo interno definisce un Vertical Panel principale (mainPanel) che contiene a sua volta:

una label per la selezione dell'ontologia; un Horizontal Panel per gli elementi di loading della ontologia (combobox e loadButton); una Label di stato per indicare i vari stati dell'applicazione (Ontologia caricata, tassonomia caricata etc.); un TaxonomyTree inizialmente vuoto che verrà popolato al primo caricamento di una delle ontologie previste.

Infine l'entryPoint definisce anche un DialogBox (dbox) utilizzato per l'evento di click sugli item del TaxonomyTree. Il taxonomyTree viene infatti settato dall'entryPoint con un clickHandler che mostra il dialog box contenente nome e descrizione della classe selezionata:

class TaxonomyTreeItemHandler implements ClickHandler, MouseOverHandler {

    @Override
    public void onClick(ClickEvent event) {
        Label itemLabel = (Label)taxonomyTree.
                getSelectedTaxonomyItem().getWidget();
        VerticalPanel vpanel = new VerticalPanel();
        HTML nameLabel = new HTML("<b>Class name: </b>"+itemLabel.getText());
        HTML descriptionLabel = new HTML("<b>Class descritpion:
                </b>"+itemLabel.getTitle());
        Button closeButton = new Button("Close");
        closeButton.getElement().setId("closeButton");
        closeButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                dbox.hide();
                dbox.clear();
            }
        });
        vpanel.add(nameLabel);
        vpanel.add(descriptionLabel);
        vpanel.add(closeButton);
        dbox.add(vpanel);
        dbox.center();
    }

L'handler viene settato per mezzo dell'apposito setter definito nel TaxonomyTree:

...
        TaxonomyTreeItemHandler itemHandler = new TaxonomyTreeItemHandler();
        taxonomyTree.setTaxonomyItemClickHandler(itemHandler);
...

Infine è stato definito un semplice CSS che per lo scopo di questo articolo non viene mostrato ma che è incluso nel progetto. Le ontologie invece per semplicità vengono caricate localmente come si può vedere dagli url predefiniti nella combobox e quindi sono state collocate nella cartella /war/ontologies/ del progetto.

Struttura della cartella web con ontologie e file css
Struttura della cartella web con ontologie e file css

Conclusione

GWT si dimostra un'ottima scelta per creare interfacce di navigazione semantiche. In particolare la possibilità di definire dei widget che implementino una logica di interfaccia specifica per il rendering di informazioni semantiche complesse e strutturate permette di creare sistemi di navigazione avanzati e intuitivi per l'utente finale, il quale, come ho già sostenuto nel primo articolo, non dovrebbe sapere se sta consultando una ontologia piuttosto che un DB o un file XML... ma dovrebbe solamente percepire i benefici e vantaggi da un tipo di navigazione delle informazioni più intelligente e coerente.

L'esempio del TaxonomyTree è un modo molto semplice per mostrare come i due aspetti, Semantic Web e interfaccie AJAX, possano al giorno d'oggi essere facilmente combinate per creare componenti grafici avanzati. Lo stesso taxonomyTree potrebbe essere ulteriormente migliorato introducendo le istanze ed eventualmente la possibilità di seguire percorsi diversi rispetto a quello gerarchico, per esempio rispetto alle proprietà. Si potrebbe inoltre arricchire il TaxonomyTree con un box di ricerca intelligente che permetta di accedere rapidamente alle informazioni contenute nell'ontologia. E tutto questo ovviamente varrebbe per qualsiasi ontologia OWL accessibile sul web. Parte di questo lavoro ho già avuto modo di sperimentarla positivamente come ricercatore presso l'ENEA sul progetto OntologyExplorer [OEXP] di cui ho già parlato nel primo articolo della serie [MOKA1].

In conclusione di questa serie vorrei far passare principalmente il concetto che tanto è stato fatto fino ad ora per standardizzare ontologie e fornire tecnologie in grado di maneggiarle ma molto resta da fare per rendere facilmente accessibili le informazioni modellate al loro interno. Gli strumenti ci sono, i tempi sono maturi... e GWT è un ottimo mezzo! Per cui... buon lavoro!

Riferimenti

  • [MOKA1] Matteo Busanelli, "Semantic Web. Esplorazione/visualizzazione di ontologie - I parte: Introduzione e analisi dei tool attuali", MokaByte 142, luglio/agosto 2009

http://www2.mokabyte.it/cms/article.run?articleId=KRJ-HZF-NP5-B8X7f00000128035691_7518ac38

  • [MOKA2] Giovanni Puliti, "Il programmatore e le sue API - XX parte: realizzare la GUI web con GWT, MokaByte 146, dicembre 2009

http://www2.mokabyte.it/cms/article.run?articleId=J3E-HC5-3JC-XGL7f00000118359738_834bfb23

  • [MOKA3] Ivan Diana, "Google Web Toolkit - I parte: Introduzione al framework GWT", MokaByte 139, aprile 2009

http://www2.mokabyte.it/cms/article.run?articleId=ZRN-948-15E-2M57f00000123998236_9088b3a8

  • [GECLIPSE] Google Eclipse Plugin

http://code.google.com/intl/it-IT/eclipse/

  • [JENA] A Semantic Web Framework for Java

http://jena.sourceforge.net/

  • [OWL] OWL Web Ontology Language Guide

http://www.w3.org/TR/owl-guide/

  • [OEXP] Ontology Explorer Project

http://www.cross-lab.it/cross-lab/imple/pgcl.asp?p=247&CMS=moda-ml-cms&lingua=en