MPS 2021.2 Help

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  .

 

Using Open API

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 LanguageLanguages 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.

Some API details

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

Meta-model level

SAbstractConcept

  • 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

SProperty

  • represents a property

  • represents a containment (parent-child) relationship between concepts

  • represents an explicit (reference) relationship between concepts

Model level

SNode

  • represents individual nodes in the model

SReference

  • represents references between nodes

SNodeReference

  • a unique global reference to a node that can be persisted and used repeatedly to obtain a node from a repository

Open API usage patterns

Setting a property

Iterable<SProperty> properties = concept/Shape/.getProperties();  list<SProperty> props = new arraylist<SProperty>(copy: properties);  currentNode/.setProperty(props.findFirst({~it => it.getName() :eq: "foo"; }), "value");

Adding a child

Iterable<SContainmentLink> containmentLinks = concept/Shape/.getContainmentLinks();  list<SContainmentLink> containments = new arraylist<SContainmentLink>(copy: containmentLinks); currentNode/.addChild(containmentLinks.findFirst(...), childNode);

Using commands

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:

  1. models and nodes can be changed through  undoable actions

  2. modules, their properties and dependencies can be performed through  repository commands

  3. 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.

Concurrent access

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.

Model-locking example

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; }  

Custom persistence

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.

Alternative file format

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.

Alternative storage

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.

Custom Find Usage and Navigation participants

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.

Last modified: 14 September 2021