Open API - accessing models from code
The language repository, project modules, languages and models can be conveniently accessed programmatically through Open API. Open API gives you controlled access to the model and also allows you to provide your own implementations for some aspects, such as persistence.
These two usage types will be discussed individually.
Note: This document is meant to provide a general high-level overview of the Open API philosophy and give you useful starting links to the API. For technical details on how to use it please consult .
tip
For hands-on code examples of using the API please check out Using model & module dependencies FAQ
The API is located under the org.jetbrains.mps.openapi package and is divided into several sub-packages:
event - contains event classes for changes to the model
language - provides a set of interfaces to gain access to compiled languages and inspect their structure
model - gives you ways to inspect and modify the models (ASTs)
module - abstracts repositories and modules as means to logically organize models
persistence - holds the interfaces necessary to extend and customize the persistence mechanism for models
repository - holds listeners to repository-specific events
util - contains utility classes, such as ProgressMonitor to hook long-lasting actions to UI progress indicators
The API recognizes these logical elements, with the ones above containing the ones below in the list:
Repository
Module
Model
Node
Properties, References
Open API also recognizes meta-structure, which is orthogonal to the elements above. The meta-structure consists of the following key elements:
Language
Concept, Enumeration
Members (such as properties, links and enum literals)
Each node has an associated concept. A concept belongs to a Language . Languages may keep a pointer to the source module that they originated from to give the language user a way to investigate the language in detail.
The API enables you to browse the whole repository and investigate its modules, their models as well as the nodes that these models are built from. Additionally the API has capabilities to search for element's usages or find any element by its name irrespective of its location within the repository. You can also modify all of these elements, save your changes or reload them from a persistent storage. The API will detect colliding modifications to the model in memory and its persistent storage.
Downcast from smodel types to Open API types happens automatically when you assign an expression typed to one of the smodel types to a variable typed to an Open API type:
node<> n1 = ...
SNode n2 = n1
Up-casting happens when you do the opposite:
SNode n1 = ...
node<> n2 = n1
a common super-class to both SConcept and SConceptInterface
gives access to concept's properties, containment links and reference links
use the getProperties(), getContainmentLinks() and getReferenceLinks() methods to obtain concept's properties, containment and reference links, respectively
represents a property
represents a containment (parent-child) relationship between concepts
represents an explicit (reference) relationship between concepts
represents individual nodes in the model
represents references between nodes
a unique global reference to a node that can be persisted and used repeatedly to obtain a node from a repository
Iterable<SProperty> properties = concept/Shape/.getProperties();
list<SProperty> props = new arraylist<SProperty>(copy: properties);
currentNode/.setProperty(props.findFirst({~it => it.getName() :eq: "foo"; }), "value");
Iterable<SContainmentLink> containmentLinks =
concept/Shape/.getContainmentLinks();
list<SContainmentLink> containments = new arraylist<SContainmentLink>(copy:
containmentLinks);
currentNode/.addChild(containmentLinks.findFirst(...), childNode);
Open API provides means to alter the models, as well. Modifications need to be performed as commands that are passed to the repository for processing. Typical model-changing/editing actions can be un-done/re-done, while actions performing major changes to the module structure cannot. There are three types of changes:
models and nodes can be changed through undoable actions
modules, their properties and dependencies can be performed through repository commands
radical changes to the project, such as a VCS update or a complete project reload, need an external update action to be performed - no node-level notifications are fired in such cases, only model replaced or module changed notifications are triggered.
The commands depending on their type will have all the necessary read or write permission assigned automatically before they start changing the model. Change notifications are fired to the registered listeners on the node, model, module or repository levels.
Open API is designed for concurrent access and will correctly handle multiple threads accessing the models through Open API simultaneously, provided the supplied synchronization mechanisms are correctly utilized by the calling code. Open API will reject all improperly synchronized requests and thus preserve the integrity of the models.
In a more concrete terms, you need to obtain a read or write action before you start performing your operations, otherwise you'd get exceptions fired from the code.
Use SRepository. getRepositoryAccess() .applyChanges() to have your changes applied to the whole repository SRepository. getModelAccess() .runXXXAction() to run a read/write action and SRepository. getModelAccess() .executeCommand() to run a command. Commands get all write permissions automatically and so always gain exclusive access to the repository. Both methods offer an asynchronous variant that runs the supplied action asynchronously in the EDT.
SRepository is the right place to start locking the model. You can obtain an SRepository reference from context objects, such as EditorContext. Your code can than obtain ModelAccess and get the lock:
(node, editorContext)->JComponent {
//get ModelAccess from the context
final ModelAccess modelAccess = editorContext.getRepository().getModelAccess();
//read the model
final boolean[] active = new boolean[]{false};
modelAccess.runReadAction(new Runnable() {
public void run() {
active[0] = node.showActive;
}
});
final JButton button = new JButton(active[0] ? "Show all" : "Show active");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent p0) {
//update the model in an action, that can be undone
modelAccess.executeCommand(new Runnable() {
public void run() {
node.showActive = !node.showActive;
}
});
}
});
return button;
}
By default MPS stores models as flat files in an XML or binary format. To allow you to customize the way your models are persisted, Open API provides gives you several options.
Changing the format of model data stored in a flat file is the simplest way to customize model persistence. You simply register your own ModelFactory with the file extension of your choice (through PersistenceFacade) and MPS will use that factory to instantiate your custom SModel implementations whenever that file extension is discovered. You'll also need to provide and register your own implementations of SModelIdFactory and SNodeIdFactory.
If you're more adventurous and want, for example, to load your models from a database or other non-file storage, you need to additionally provide a ModelRootFactory, which can create custom ModelRoot instances. These model roots will then handle all the specifics of your chosen storage in order to load/save models. You may typically also need to bundle UI that would allow the users to configure data source details, such as database location, user name or other.
tip
The xmlPersistence sample project that comes bundled with MPS shows a non-trivial example of implementing custom persistence. Check out the description of the Custom Persistence Cookbook for practical details on how to provide your own persistence to MPS models in MPS as well as IDEA.
Providing a custom implementation of FindUsagesParticipant will allow you to optimize FindUsages in models using your custom persistence. Similarly, custom implementations of NavigationParticipant will have a chance to optimize Go To Root/Class/Symbol actions. Instead of letting the default find usages and navigation implementations load all models into memory and process them in a standard way, by providing custom participants to PersistenceFacade you have the option to access the persistent storage directly and thus speed-up search as well as navigation.
Thanks for your feedback!