Org.simantics.objmap Manual

From Developer Documents
Jump to navigation Jump to search

Introduction

When implementing an editor in Simantics platform, it is very common that the graph representation cannot be directly used, but the editor needs to create an intermediate model of the subgraph it edits. Some reasons for this:

  • Accessing the database directly is not fast enough.
  • An editor using the database directly holds frequently long read locks and cannot operate during write transactions.
  • The editor needs to store auxiliary objects attached to the model.
  • Editor modifies the intermediate model speculatively before the modification is committed to the database or canceled.
  • The modifications in database cannot be applied in the editor immediately (for example during rendering).
  • Third-party component requires certain classes to be used.
  • Editor needs to be backend agnostic and cannot directly use database api.

There are two different approaches for implementing the intermediate model:

Triangle model
The editor modifies the database directly and the changes in the database are eventually propagated to the intermediate model. The editor doesn't change the intermediate model directly.

TriangleModel.png

Bidirectional model
The editor operates only using the intermediate model and modifications are updated from intermediate model to the database and vice versa.

BidirectionalModel.png

By experience, the triangle model is easier to implement correctly in particular when resources are linked to each other in complex fashion. The org.simantics.objmap-plugin tries to make the implementation of bidirectional model easier by providing a framework for defining declaratively the update procedure between database and intermediate model. It can also be used with triangle model only for one direction or with hybrid model where some operations are implemented using the intermediate model and other modifying the database directly.

Design principles

Symmetric
For every operation from database to Java objects there is a corresponding operation from Java objects to database. This makes the framework easier to learn and undestand.
Non-intrusive
The Java objects used with the framework do not need to implement any specific interface or follow some specific naming convention. The mapping schema can be defined using annotations or separately from the class definition.
Support for different use scenarios
  • bidirectional / unidirectional
  • one shot / continuous
  • automatic listening / manual updating
One-to-one
For every resource there is a single corresponding Java object and vise versa. This makes the framework easier to understand. It is not a transformation framework.

Concepts

Mapping consists of a set of resources called a domain, a set of Java objects called a range and a collection of links. Each link is attached to exactly one domain and range element and each domain and range element has at most one link attached to it. Additionally the link has a link type that contains requirements for the domain and range elements in the same link.

ObjectMappingTerminology.png

A mapping is up-to-date if every domain and range element has a link and all links satisfy the requirements of their link types. The links of up-to-date mapping form a bijection from domain to range.

A mapping schema associates all domain and range elements with a link type. It is used to add new domain and range elements to the mapping.

Mapping interface

The plugin represents a mapping with interface org.simantics.objmap.IMapping. The interface is symmetric in the sense that every operation on the domain of the mapping has also a counterpart that operates on the range. Typically, if one of the operations requires a read graph, its counterpart requires a write graph. We will describe only the methods operating on the domain of the mapping:

Set<Resource> getDomain();

Returns the domain of the mapping. All set operations are supported. Adding a new domain element does not automatically create a link to it. Removal of a domain element removes also a link and the target element, but does not remove the element from the database.

Collection<Resource> updateDomain(WriteGraph g) throws MappingException;

Updates all domain elements whose counterpart is modified and creates new domain elements for previously added range elements. Returns the collection of domain elements that were modified or created in the update process.

Object get(Resource domainElement);

Returns the counterpart of a domain element or null if the element does not belong to the domain or does not have a link.

Object map(ReadGraph g, Resource domainElement) throws MappingException;

A convenience method that adds a domain element to the mapping and immediately updates the mapping and returns the corresponding range element.

void domainModified(Resource domainElement);

Tells the mapping that the domain element has been modified.

boolean isDomainModified();

Tells if some domain elements have been modified or added.

Collection<Resource> getConflictingDomainElements();

Returns a collection of domain elements which have been modified and also their counterparts in the mapping are modified. These elements are in conflict in the sense that the updating domain and range in different orders may produce different results.

void addMappingListener(IMappingListener listener);
void removeMappingListener(IMappingListener listener);

Adds or removes a listener for domain and range modifications.

Defining a mapping schema

The primary way for defining a mapping schema is to use Java annotations. The current annotation support is still lacking. Only the following annotations are supported:

GraphType(uri)
Specifies the domain type that the class corresponds to.
RelatedValue(uri)
Specifies a correspondence between a field and functional property.
RelatedElement(uri)
Specifies a correspondence between a field and functional relation
RelatedElements(uri)
Specifies a correspondence between a field and a relation. The type of the field has to be a collection.

Example

Suppose we have the following annotated classes:

@GraphType("http://www.simantics.org/Sysdyn#Configuration")
static class Configuration {
    @RelatedElements("http://www.vtt.fi/Simantics/Layer0/1.0/Relations#ConsistsOf")
    Collection<Component> components; 
}

static abstract class Component {        
}

@GraphType("http://www.simantics.org/Sysdyn#Dependency")
static class Dependency extends Component {
    @RelatedElement("http://www.simantics.org/Sysdyn#HasTail")
    Variable tail;
    @RelatedElement("http://www.simantics.org/Sysdyn#HasHead")
    Auxiliary head;
}

static abstract class Variable extends Component {
    @RelatedValue("http://www.vtt.fi/Simantics/Layer0/1.0/Relations#HasName")
    String name;
}

@GraphType("http://www.simantics.org/Sysdyn#Auxiliary")
static class Auxiliary extends Variable {
}

Them the schema can be defined as follows:

SimpleSchema schema = new SimpleSchema();
schema.addLinkType(MappingSchemas.fromAnnotations(g, Configuration.class));
schema.addLinkType(MappingSchemas.fromAnnotations(g, Dependency.class));
schema.addLinkType(MappingSchemas.fromAnnotations(g, Auxiliary.class));

Using the mapping interface

Assume that a mapping scheme scheme has already been defined and modelRoot is the root resource of the model that the editor edits. Then the model is created as follows:

IMapping mapping = Mappings.create(scheme);
in read transaction {
    MyModel model = (MyModel)mapping.map(graph, modelRoot);
} 

There are different ways how the mapping can be updated. The following code forces update for all domain elements.

in read transaction {
    for(Resource r : mapping.getDomain())
        mapping.domainModified(r);
    mapping.updateRange(graph);
}

If the range elements have some kind of "dirty" flags, the update can be optimized:

in write transaction {
    for(Object obj : mapping.getRange())
        if(obj implements MyObject && ((MyObject)obj).isDirty())
            mapping.rangeModified(obj);
    mapping.updateDomain(graph);
}

Often the editor has to update some auxiliary structures when the mapping modifies the range. This can be implemented for example as:

for(Object obj : mapping.updateRange(graph))
    if(obj implements MyObject)
        ((MyObject)obj).updateAuxiliary();

The most convenient way for updating the target would be to add graph request listeners for each domain element in the mapping. This is not yet implemented although the current interface should support this without modifications. Currently the only way to listen the database changes is to listen the request that is used to call the updateRange-method.