Creating and Working with Nodes in LionWeb
LionWeb provides a flexible and language-agnostic model for working with models (or trees, or ASTs: let's consider these as synonyms in this context).
The main component is the Node.
When working with LionWeb nodes in Java, there are two complementary approaches depending on your needs:
- Homogeneous nodes, using generic, universal APIs which work with all form of nodes. When choosing this approach, we may want to consider
DynamicNode
. - Heterogeneous nodes, using language-specific, statically-typed Java classes, defined for a certain LionWeb language and just that one.
The Core Abstraction: Node
At the heart of LionWeb is the Node
interface. Implementing it guarantees:
- Serialization and deserialization
- Compatibility with the LionWeb Repository
- Introspection through classifiers and features
- Tool support (e.g., editors, model processors)
By relying on this interface, LionWeb tooling can process, manipulate, and analyze any conforming node in a uniform manner.
Option 1: Homogeneous Nodes
This approach is ideal for generic tools and runtime interoperability. The key class here is DynamicNode
.
When to Use
- You receive nodes from external systems or clients
- You want to handle unknown or dynamic languages
- You’re building generic tools (e.g., validators, browsers)
How it Works
DynamicNode
implements Node
and stores features dynamically. You can query and manipulate the node’s structure by name.
Example
package com.example;
import io.lionweb.lioncore.java.language.*;
import io.lionweb.lioncore.java.model.ClassifierInstanceUtils;
import io.lionweb.lioncore.java.model.Node;
import io.lionweb.lioncore.java.model.impl.DynamicNode;
import io.lionweb.lioncore.java.utils.NodeTreeValidator;
import io.lionweb.lioncore.java.utils.ValidationResult;
import java.util.List;
public class HomogeneousAPIExample {
private static Concept taskListConcept;
private static Concept taskConcept;
private static Property nameProperty;
private static Containment tasksContainment;
private static Language taskLanguage;
{
defineLanguage();
}
public static void defineLanguage() {
// Define the 'TaskList' concept
taskListConcept = new Concept("TaskList");
taskListConcept.setID("TaskList-id");
taskListConcept.setName("TaskList");
taskListConcept.setKey("TaskList");
taskListConcept.setAbstract(false);
taskListConcept.setPartition(true);
// Define the 'Task' concept
taskConcept = new Concept("Task");
taskConcept.setID("Task-id");
taskConcept.setName("Task");
taskConcept.setKey("Task");
taskConcept.setAbstract(false);
// Add a 'tasks' containment
tasksContainment = new Containment()
.setID("TasksList-tasks-id")
.setName("tasks")
.setKey("TasksList-tasks")
.setMultiple(true)
.setOptional(false)
.setType(taskConcept);
taskListConcept.addFeature(tasksContainment);
// Add a 'name' property
nameProperty = new Property();
nameProperty.setID("task-name-id");
nameProperty.setName("name");
nameProperty.setKey("task-name");
nameProperty.setType(LionCoreBuiltins.getString());
taskConcept.addFeature(nameProperty);
// Define the language container
taskLanguage = new Language();
taskLanguage.setID("task-id");
taskLanguage.setKey("task");
taskLanguage.setName("Task Language");
taskLanguage.setVersion("1.0");
taskLanguage.addElement(taskListConcept);
taskLanguage.addElement(taskConcept);
}
public void useDynamicNode() {
// Create the model
DynamicNode errands = new DynamicNode("errands", taskListConcept);
DynamicNode task1 = new DynamicNode("task1-id", taskConcept);
task1.setPropertyValue(nameProperty, "My Task #1");
errands.addChild(tasksContainment, task1);
DynamicNode task2 = new DynamicNode("task2-id", taskConcept);
task2.setPropertyValue(nameProperty, "My Task #2");
errands.addChild(tasksContainment, task2);
ValidationResult res = new NodeTreeValidator().validate(errands);
if (!res.isSuccessful()) {
throw new IllegalStateException("The tree is invalid: " + res);
}
// Access the model
List<Node> tasks = errands.getChildren(tasksContainment);
System.out.println("Tasks found: " + tasks.size());
for (Node task : tasks) {
System.out.println(" - " + task.getPropertyValue(nameProperty));
}
// Access the model, using ClassifierInstanceUtils
List<? extends Node> tasksAgain = ClassifierInstanceUtils.getChildrenByContainmentName(errands, "tasks");
System.out.println("Tasks found: " + tasksAgain.size());
for (Node task : tasksAgain) {
System.out.println(" - " + ClassifierInstanceUtils.getPropertyValueByName(task, "name"));
}
}
public static void main(String[] args) {
new HomogeneousAPIExample().useDynamicNode();
}
}
Evaluation
- No static typing
- No compile-time safety
- No code completion or type checking
- Work out of the box, without the need to write any code for each language
If you misspell "name"
or access a non-existent feature, you’ll get a runtime exception.
Option 2: Heterogeneous Nodes
This approach is recommended when building interpreters, compilers, or other tools for a specific language.
You define a Java class for each concept, typically:
- Implementing the
Node
interface - Optionally extending
DynamicNode
for convenience
But how can you define these classes?
Of course, you can do that in the good old way: writing the code yourself.
Or you can define a code generator which, given a language, produce the corresponding classes. This may also be a feature we eventually implement in LionWeb Java.
When to Use
- You are building tooling for a specific DSL or language
- You want type-safe code with IDE support
- You require structured, validated access to features
Example
package com.example;
import io.lionweb.lioncore.java.language.*;
import io.lionweb.lioncore.java.model.impl.DynamicNode;
import io.lionweb.lioncore.java.utils.NodeTreeValidator;
import io.lionweb.lioncore.java.utils.ValidationResult;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class HeterogeneousAPIExample {
private static Concept taskListConcept;
private static Concept taskConcept;
private static Property nameProperty;
private static Containment tasksContainment;
private static Language taskLanguage;
{
defineLanguage();
}
private class TaskList extends DynamicNode {
TaskList() {
super(UUID.randomUUID().toString(), taskListConcept);
}
void addTask(Task task) {
addChild(tasksContainment, task);
}
List<Task> getTasks() {
return getChildren(tasksContainment).stream().map(n -> (Task)n).collect(Collectors.toList());
}
}
private class Task extends DynamicNode {
Task(String name) {
super(UUID.randomUUID().toString(), taskConcept);
setName(name);
}
void setName(String name) {
setPropertyValue(nameProperty, name);
}
String getName() {
return (String) getPropertyValue(nameProperty);
}
}
public static void defineLanguage() {
// Define the 'TaskList' concept
taskListConcept = new Concept("TaskList");
taskListConcept.setID("TaskList-id");
taskListConcept.setName("TaskList");
taskListConcept.setKey("TaskList");
taskListConcept.setAbstract(false);
taskListConcept.setPartition(true);
// Define the 'Task' concept
taskConcept = new Concept("Task");
taskConcept.setID("Task-id");
taskConcept.setName("Task");
taskConcept.setKey("Task");
taskConcept.setAbstract(false);
// Add a 'tasks' containment
tasksContainment = new Containment()
.setID("TasksList-tasks-id")
.setName("tasks")
.setKey("TasksList-tasks")
.setMultiple(true)
.setOptional(false)
.setType(taskConcept);
taskListConcept.addFeature(tasksContainment);
// Add a 'name' property
nameProperty = new Property();
nameProperty.setID("task-name-id");
nameProperty.setName("name");
nameProperty.setKey("task-name");
nameProperty.setType(LionCoreBuiltins.getString());
taskConcept.addFeature(nameProperty);
// Define the language container
taskLanguage = new Language();
taskLanguage.setID("task-id");
taskLanguage.setKey("task");
taskLanguage.setName("Task Language");
taskLanguage.setVersion("1.0");
taskLanguage.addElement(taskListConcept);
taskLanguage.addElement(taskConcept);
}
public void useSpecificClasses() {
// Create the model
TaskList errands = new TaskList();
Task task1 = new Task("My Task #1");
errands.addTask(task1);
Task task2 = new Task("My Task #2");
errands.addTask(task2);
ValidationResult res = new NodeTreeValidator().validate(errands);
if (!res.isSuccessful()) {
throw new IllegalStateException("The tree is invalid: " + res);
}
// Access the model
List<Task> tasks = errands.getTasks();
System.out.println("Tasks found: " + tasks.size());
for (Task task : tasks) {
System.out.println(" - " + task.getName());
}
}
public static void main(String[] args) {
new HeterogeneousAPIExample().useSpecificClasses();
}
}
Evaluation
- Full IDE support (auto-completion, navigation)
- Catch errors at compile time
- Clear API for collaborators
- Require extra work for defining the classes
Suggested approach
- Use
DynamicNode
in model editors, importers, migrators - Use custom classes (like
PersonNode
) in interpreters, generators, type checkers
Interoperability
Both approaches can co-exist. For example, you might parse a file into DynamicNode
objects and then convert them into typed classes using a factory or builder.