Für die meisten ist ein Baum eine holzige Pflanze. Aber in der Informatik versteht man darunter oben skizzierte Datenstruktur, mit der sicherlich jeder von uns schon mal etwas zu tun hatte. Dieses Tutorial soll zeigen, wie man mit Spring Roo einen Baum implementiert und ihn mithilfe des Dojo Frameworks anzeigt. Zusätzlich sollen die Knoten dynamisch beim auf und zu klappen des Baumes per JSON nachgeladen werden, um somit auch große Bäume performant darstellen zu können (Lazy Loading).

Eine Baumstruktur ist mit Roo relativ einfach abzubilden. Zunächst erstellen wir ein Projekt, mit einer Entity Node, die einen Knoten (bzw. ein Blatt) im Baum repräsentiert. Dem Knoten geben wir dann noch ein Feld name. Das reicht, mehr Entities werden wir nicht brauchen.

[bash]
mkdir tree
cd tree
roo
project –topLevelPackage de.scandio.tree
persistence setup –provider HIBERNATE –database HYPERSONIC_IN_MEMORY
entity –class ~.Node
field string –fieldName name
controller all –package ~.web
perform eclipse
exit
[/bash]

Mit mvn:tomcat run sollten sich der Server nun starten lassen. Unter “Create Node” können wir Nodes anlegen und/oder löschen. Aber die Objekte sind noch nicht, wie in einer Baumstruktur, miteinander verkettet. Jeder Knoten in einem Baum hat einen Vater-Knoten und ein, mehrere, oder eventuell keine Kinder-Knoten. Für die Verkettung müssen wir mit Roo weitere Felder hinzufügen:

[bash]
roo
focus –class ~.Node
field reference –fieldName parent –type ~.Node
field set –fieldName children –type ~.Node –mappedBy parent –cardinality ONE_TO_MANY
exit
[/bash]

Mit field reference wird eine Referenz auf den Vater-Knoten, mit Namen parent, angelegt. Die Baumstruktur wäre damit schon abgebildet. Wir legen aber noch ein Set children an, um bequemer auf die Kinder zugreifen zu können. Mit dem Parameter mappedBy wird festgelegt, dass das Feld parent der Besitzer dieser Relation ist. Mit cardinality wird die Kardinalität angegeben. Hier steht ein Vater mit mehreren Kindern in Beziehung, deshalb “ONE_TO_MANY”.

Unsere Java-Klasse wird von Roo generiert und schaut so aus:

@RooJavaBean
@RooToString
@RooEntity
public class Node {
    private String name;
    @ManyToOne
    private Node parent;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    private Set<de.scandio.tree.Node> children = new HashSet<de.scandio.tree.Node>();
}

Damit können wir nun Knoten, unter Angabe eines Vaters, anlegen. Der erste Knoten (der Root-Knoten) hat natürlich keinen, in der Datenbank wird daher NULL stehen.

Jetzt wird’s spannend. Wir wollen unseren Baum in einem Dojo Tree anzeigen. Mit dem Dojo Tree hat man die Möglichkeit Knoten auf und zu zuklappen, wie in der Baum-Ansicht eines Datei-Managers.

Der Tree ist jedoch nur eine Ansicht des Baumes. Der Baum an sich, wird in einem Model abgebildet. Dojo bietet ein TreeStoreModel und ein ForestStoreModel an. Der Unterschied zwischen den beiden ist, dass ein ForestStoreModel auch mehere Root-Knoten haben kann. Wir werden den letzteren benutzen, auch wenn wir in diesem Beispiel nur einen Root-Knoten haben. Man weiß ja nie…

Das Model wiederum bekommt seine Daten aus einem Data Store. Dojo bietet eine vielzahl von Data Stores an. Wir werden hier den JsonRestStore verwenden. Das heisst, dass unsere Knoten per JSON-Requests abgerufen werden.

Der JsonRestStore bietet ein nettes Feature: JSON Referencing. Mit JSON Referencing kann ein Objekt andere Objekte referenzieren. Was bringt uns das bei unserem Baum? Die Idee ist, dass beim Initialisieren des Baumes, nicht der komplette Baum per JSON abgerufen wird, sondern dynamisch nur die Knoten, die auch wirklich angezeigt werden. Das heisst, dass beim Aufruf zunächst nur der Root-Knoten angezeigt wird. Der JSON-String ohne Referencing würde so aussehen:

{"id":1,"name":"a"}

Damit der Baum nun weiß, was er anzeigen soll, wenn man den Knoten aufklappt, kommt das Referencing ins Spiel:

{"id":1,"name":"a","childrenRef":[{"$ref":"2"},{"$ref":"3"}]}

Das Feld childrenRef ist ein Array und enthält Referenzen auf die Kinder (hier einfach die ID). JSON Referencing legt fest, dass das Atrribut “$ref” heissen muss (man beachte das Dollar-Zeichen). Klappt man nun den Root-Knoten auf, weiß der JsonRestStore, dass er die Knoten mit ID 2 und 3 nachladen muss.

Desweiteren empfiehlt es sich ein Attribut einzuführen, das angibt ob ein Knoten Kinder besitzt, oder nicht. Das dient dazu um einen Knoten dementsprechend als Knoten oder as Blatt zu zeichnen. Wir nennen dieses Attribut hasChildren. So schaut nun der komplette JSON-String eines Knotens aus:

{"id":1,"name":"a","hasChildren":true,"childrenRef":[{"$ref":"2"},{"$ref":"3"}]}

Nun zur Implementierung. Spring Roo bietet Unterstützung für JSON. Das ist schon mal gut. Aber wir können Node Objekte nicht einfach so serialisieren, denn bei den Kindern stößt man auf ein Problem. Serialisiert man die Collection nicht mit, hat der Baum keine Informationen über die Kinder eines Knotens. Serialisiert man sie mit, serialisiert man unter Umständen den Kompletten Ast der von dem Knoten ausgeht. Also die Kinder eines Knotens, und deren Kinder, usw. Was wir aber wollen ist nur die direkten Kinder , und am besten mit dem Attribut $ref, damit das Referencing funktioniert.

Deshalb verhelfen wir uns mit zwei Wrapper-Klassen. JsonNode und JsonNodeRef. JsonNodeRef repräsentiert eine Referenz auf ein JsonNode und enthält nur ein Attribut: $ref. JsonNode repräsentiert ein Node, mit dem Unterschied, dass sie kein Set von Nodes enthält, sondern ein Set von JsonNodeRefs.

JsonNodeRef:

@RooJavaBean
public class JsonNodeRef {
	private String $ref;
	public JsonNodeRef(Node node) {
		this.$ref = node.getId().toString();
	}
}

JsonNode:

@RooJavaBean
public class JsonNode {
	private Long id;
	private String name;
	private boolean hasChildren;
	private Collection<JsonNodeRef> childrenRef = new HashSet<JsonNodeRef>();
    public JsonNode(Node node) {
    	this.id = node.getId();
    	this.name = node.getName();
    	this.hasChildren = !node.getChildren().isEmpty();
    	for(Node child: node.getChildren()) {
			this.childrenRef.add(new JsonNodeRef(child));
		}
    }
}

Für jedes Kind wird ein JsonNodeRef Objekt erstellt und zum Set childrenRefs hinzugefügt. Zusätzlich enthält JsonNode das Attribut hasChildren.

Jetzt fehlt noch der JSON-Controller. Dazu müssen wir der Klasse JsonNode beibringen sich zu JSON zu serialisieren.

@RooJavaBean
public class JsonNode {
	private Long id;
	private String name;
	private boolean hasChildren;
	private Collection<JsonNodeRef> childrenRef = new HashSet<JsonNodeRef>();
    public JsonNode(Node node) {
    	this.id = node.getId();
    	this.name = node.getName();
    	this.hasChildren = !node.getChildren().isEmpty();
    	for(Node child: node.getChildren()) {
			this.childrenRef.add(new JsonNodeRef(child));
		}
    }
    public String toJson() {
    	return new JSONSerializer().exclude("*.class").include("childrenRef").serialize(this);
    }
    public static String toJsonArray(Collection<JsonNode> collection) {
    	return new JSONSerializer().exclude("*.class").include("childrenRef").serialize(collection);
    }
    public static Collection<JsonNode> findJsonNodeRoot() {
    	Collection<JsonNode> children = new TreeSet<JsonNode>();
    	for(Node child: Node.findRootNodes().getResultList()) {
    		children.add(new JsonNode(child));
    	}
    	return children;
    }
    public static JsonNode findJsonNode(Long id) {
		Node node = Node.findNode(id);
		return new JsonNode(node);
	}
}

Das ist JSON-Serialisierung im Spring-Stil und relativ straightforward. Zu beachten ist, dass die Collection childrenRef expliziet hinzugefügt werden muss. Wir brauchen auch zwei Finder-Methoden. Eine die uns ein JsonNode zu einer ID liefert, und eine die uns alle Root-Knoten liefert. Letztere ruft einen Finder in der Klasse Node auf, die wir noch schreiben müssen:

@RooJavaBean
@RooToString
@RooEntity
public class Node {
    private String name;
    @ManyToOne
    private Node parent;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    private Set<de.scandio.tree.Node> children = new HashSet<de.scandio.tree.Node>();
    public static TypedQuery<Node> findRootNodes() {
        EntityManager em = Node.entityManager();
        TypedQuery<Node> q = em.createQuery("SELECT Node FROM Node AS node WHERE node.parent IS NULL", Node.class);
        return q;
    }
}

JSON-Controller nicht vergessen:

@RequestMapping("/json")
@Controller
public class JsonNodeController {
	@RequestMapping(value = "/root", method = RequestMethod.GET, headers = "Accept=application/json")
    @ResponseBody
	public String getRoot() {
		return JsonNode.toJsonArray(JsonNode.findJsonNodeRoot());
	}
	@RequestMapping(value = "/{id}", method = RequestMethod.GET, headers = "Accept=application/json")
    @ResponseBody
    public String get(@PathVariable("id") Long id) {
        return JsonNode.findJsonNode(id).toJson();
    }
}

Jetzt ist Server-seitig alles fertig. Was noch fehlt, ist der Dojo Tree. Dieser kann in einer beliebigen Template-Datei eingefügt werden. Ich habe ihn zu Beispiel-Zwecken einfach in die index.jspx eingefügt.

<script type="text/javascript" charset="utf-8">
    dojo.require("dojox.data.JsonRestStore");
    dojo.require("dijit.Tree");
    dojo.addOnLoad(function() {
    	var store = new dojox.data.JsonRestStore({
	    	target: "/tree/json",
	    	idAttribute:"id"
		});
        var treeModel = new dijit.tree.ForestStoreModel({
        	store: store,
            query: "root",
            childrenAttrs: ["childrenRef"],
            labelAttr: "name",
            deferItemLoadingUntilExpand: true,
            mayHaveChildren: function(item)	{
            	return store.getValue(item, "hasChildren") == true;
        	}
        });
        tree = new dijit.Tree({
            model: treeModel,
            showRoot: false,
            onClick: function(item, node) {
	            location.href = "/tree/nodes/" + item.id;
	        }
        },
        "tree");
    });
    </script>
    <div id="tree"></div>
[/javascript]
Es wird zunächst der JsonRestStore mit dem Pfad zum JSON-Controller initialisiert. Dannach das ForestStoreModel, dem der Store übergeben wird. Die hier gesetzten Attribute können in der Dokumentation nachgelesen werden. Wichtig ist nur, dass deferItemLoadingUntilExpand auf true gesetzt ist, damit die Knoten wirklich erst beim Aufklappen geladen werden. Zu guter Letzt wird der Tree mit dem Model instanziert.
Und so schaut unser Baum aus:

Der komplette Quelltext dieses Tutorials befindet sich auf Github:
https://github.com/scandio/RooTreeExample