Skip to content

Plugin Step by Step Tutorial

Patrik Meijer edited this page Oct 10, 2017 · 31 revisions

Prerequisites

  • Nodejs and mongodb, see main README.md
  • webgme-cli needs to be installed globally (check by typing webgme --version, if not do npm install -g webgme-cli)
  • You need a repository initialized by the webgme-cli (if not, in an empty directory do webgme init followed by npm install (and npm install webgme for npm > 3))
  • A generated plugin via webgme new plugin TutorialPlugin inside the root of the repository.
  • Make sure mongodb is up and running.
  • You need nodemon installed globally (check by typing nodemon --version, if not do npm install -g nodemon)

Preloading the nodes

Then locate the following lines of code inside the generated plugin (#72 - #86).

        self.core.setAttribute(nodeObject, 'name', 'My new obj');
        self.core.setRegistry(nodeObject, 'position', {x: 70, y: 70});


        // This will save the changes. If you don't want to save;
        // exclude self.save and call callback directly from this scope.
        self.save('TutorialPlugin updated model.')
            .then(function () {
                self.result.setSuccess(true);
                callback(null, self.result);
            })
            .catch(function (err) {
                // Result success is false at invocation.
                callback(err, self.result);
            });

Make the following changes:

        // (1)
        //self.core.setAttribute(nodeObject, 'name', 'My new obj');
        //self.core.setRegistry(nodeObject, 'position', {x: 70, y: 70});


        // (2)
        self.loadNodeMap(self.rootNode)
            .then(function (nodes) {
                self.logger.info(Object.keys(nodes));
                self.result.setSuccess(true);
                callback(null, self.result);
            })
            .catch(function (err) {
                // (3)
                self.logger.error(err.stack);
                // Result success is false at invocation.
                callback(err, self.result);
            });

First of all (1) we no longer want to modify the name or position of the activeNode, so we simply comment out those two lines of code.

Secondly (2) instead of saving the changes in the model, we would like to get the map (here named nodes) generated by the loadNodeMap. In this example we would like to get all nodes inside of the project so we pass self.rootNode as the starting node from where all nodes are to be preloaded. Since the function returns a node-map we need to declare a variable in the callback, here it is named nodes. In order to make sure that everything works, we log the keys of this map.

Q1: What do the keys represent?

Q2: Are they unique?

To get better feed-back while we're developing our plugin, make sure to add the extra logging of the stack at (3).

After this point make sure that you can run the plugin using nodemon. Open up a new shell in the root of the directory and start the watcher by invoking:

nodemon ./node_modules/webgme-engine/src/bin/run_plugin.js TutorialPlugin <NameOfYourProject>

By default the active-node will be the rootNode, for more options invoke:

node ./node_modules/webgme-engine/src/bin/run_plugin.js --help

Get data about the nodes

Now when we have access to all the nodes, we can start by getting some data about the nodes without regard to their place in the project tree. Create a method that takes the nodes as the first argument. (In all methods we will start by creating a reference to this and always refer to it via self. That way we avoid to accidentally use this in a place where it no longer refers to our plugin instance.)

    TutorialPlugin.prototype.printRandomly = function (nodes) {
        var self = this,
            path,
            name,
            node;

        for (path in nodes) {
            node = nodes[path];
            name = self.core.getAttribute(node, 'name');
            self.logger.info(name, 'has path', self.core.getPath(node));
        }
        
    };

Note that in JavaScript you can iterate over the keys of an object (i.e. map) by using for (key in myObj) {...}.

The name of a node is just an attribute named 'name', so in order to get the name we request the attribute 'name' using the core. We also request to get the path of the node.

Q3: Is there another way we can obtain the path of the node in this case?

With the new method created, make sure to call it after the nodes have been obtained back in the main method.

        self.loadNodeMap(self.rootNode)
            .then(function (nodes) {
                self.logger.info(Object.keys(nodes));
                self.printRandomly(nodes);
                self.result.setSuccess(true);
                callback(null, self.result);
            })
            .catch(function (err) {
                 ...

Using the META

Now we would like to get extra information depending on the meta-type of the encountered nodes. Based on the META-model for your project, pick a meta-type that has an attribute defined and print out the value when a node of that type is encountered.

Within a plugin self.META is map between the name of the meta-type nodes and the actual nodes. We can use the method self.isMetaTypeOf to check if a given node is of the meta-type node passed in the second argument.

Below we show how this is done for Transition with an attribute named guard. (N.B. if there are special characters in the name of the meta you need to use the string approach to get to the node, e.g. self.META['Name of a Meta Type'].

    TutorialPlugin.prototype.printRandomly = function (nodes) {
        var self = this,
            path,
            name,
            attr,
            node;

        for (path in nodes) {
            node = nodes[path];
            name = self.core.getAttribute(node, 'name');
            if (self.isMetaTypeOf(node, self.META.Transition)) {
                attr = self.core.getAttribute(node, 'guard');
                self.logger.info(name, 'has guard', attr);
            }
        }
        
    };

Q4: What is printed if you remove the checking for the meta-type?

Q5: How and where are attributes defined?

Q6: When an attribute is defined, which nodes will have it?

Let extend the function by printing out the name of the meta-type if it isn't a Transition (or the type you're using).

    TutorialPlugin.prototype.printRandomly = function (nodes) {
        var self = this,
            path,
            name,
            attr,
            metaNode,
            node;

        for (path in nodes) {
            node = nodes[path];
            name = self.core.getAttribute(node, 'name');
            if (self.isMetaTypeOf(node, self.META.Transition)) {
                attr = self.core.getAttribute(node, 'guard');
                self.logger.info(name, 'has event', attr);
            } else {
                metaNode = self.getMetaType(node);
                self.logger.info(name, 'is of meta-type', self.core.getAttribute(metaNode, 'name'));
            }
        }
    };

Q7: Are there any potential issues with this? Which node does not have a meta-type?

Add a guard against this issue.

Containment hierarchy

Q8: How can we define a containment relation between two types of nodes?

Q9: Why is containment called a strong relationship?

So far we have visited all nodes in the project without any regard to their place in the containment hierarchy. Now create a method called printChildrenRec on the prototype.

    TutorialPlugin.prototype.printChildrenRec = function (root, nodes, indent) {
        var self = this,
            childrenPaths,
            childNode,
            i;

        indent = indent || '';

        childrenPaths = self.core.getChildrenPaths(root);
        self.logger.info(indent, self.core.getAttribute(root, 'name'), 'has', childrenPaths.length, 'children.');

        for (i = 0; i < childrenPaths.length; i += 1) {
            childNode = nodes[childrenPaths[i]];
            self.printChildrenRec(childNode, nodes, indent + '  ');
        }

    };

From now on we will no longer print the nodes randomly, so edit the invoked method inside of main to call this method instead.

        self.loadNodeMap(self.rootNode)
            .then(function (nodes) {
                self.printChildrenRec(self.rootNode, nodes);
                self.result.setSuccess(true);
                callback(null, self.result);
            })
            .catch(function (err) {
                 ...

Q10: Why can we leave out the third argument?

Q11: For each node determine if it is a meta-node or not (hint use the method self.getMetaType(node)).

Q12: For each child-node check if it is a connection and print the name and paths of the nodes it is connecting. (Hint self.core.getPointerPath(childNode, pointerName), where pointerName is either 'src' or 'dst'. Remember a connection is something that has a value for both of these pointers.)

Saving and returning an artifact

Modify printChildrenRec in a way such that for each encountered meta-node an entry with the name, path and number of children is added to an array:

self.metaNodeInfo.push({name: <nameOfMetaNode>, path: <pathOfMetaNode>, numberOfChildren: <numberOfChildrenOfMetaNode>});

Before pushing to the array we need to declare it. We can do that in our main function, as long as we do it before we try to add to it.

        self.metaNodeInfo = [];
        var artifact;
        self.loadNodeMap(self.rootNode)
            .then(function (nodes) {
                self.printChildrenRec(self.rootNode, nodes);
                // Here data has been added metaNodeInfo.
                var metaNodeInfoJson = JSON.stringify(self.metaNodeInfo, null, 4);
                artifact = self.blobClient.createArtifact('data');
                return artifact.addFile('metaNodeInfo.json', metaNodeInfoJson);
            })
            .then(function (fileHash) {
                self.result.addArtifact(fileHash);
                return artifact.save()
            })
            .then(function (artifactHash) {
                self.result.addArtifact(artifactHash);
                self.result.setSuccess(true);
                callback(null, self.result);
            })
            .catch(function (err) {
                 ...

Q13: What is the second and third argument of JSON.stringify?

Q14: Run the plugin in the browser, where are self.result.addArtifact showing up in the results dialog?

Q15: What is the difference between the two linked artifacts?

Answers

Q1: The paths (a.k.a.) ids of the nodes. These are a concatenation (joined by /) of the relative ids of all its parents and its own. The rootNode have an empty string as its relative id and therefor all other nodes paths start with a leading /.

Q2: At each level in the containment tree the relative ids among siblings are unique - this means that the paths are unique among all nodes.

Q3: Since we're iterating over the nodes indexed by their paths, we can simply use the path variable from the for-loop.

Q4: Assuming no meta-violations in the project and the attribute is not defined in any other type that the node is of, the returned value will be undefined.

Q5: Attributes definitions are typically added using the Meta-Editor on the UI (you can use the core-API to programatically add such too).

Q6: The definition of an attribute will be propagated to all nodes that are of the meta-type. That is all nodes that somewhere along its base chain have the given meta-type node. We also refer to this as all instances of the meta-type node. In addition the definitions will be propagated from all the mixins of the bases (however in this case those values are not).

Q7: The rootNode does not have a meta-type. It doesn't have any base nor any instances. The FCO doesn't have a base either, however it itself is part of the meta and thus is of meta-type FCO. To guard against the exception, we can either add a check if the path is the empty string or if the returned metaNode is undefined (both of these are "falsy" in javascript).

Q8: In the Meta-Editor we select the black filled diamond connection (it's selected by default) and draw a connection from the meta-type that should be able to be contained to the meta-type that should be able to contain the other node. For example if we want a StateMachine to be able to contain States we would draw the following connection: State *---<> StateMachine.

Q9: Containment is called a strong relationship since we the parent (the container) is deleted, the contained children are also deleted. (For instance if we delete the StateMachine example from the project tree, all the contained States and Transitions are deleted too.)

Q10: When not passing the trailing arguments when calling a function in JavaScript the values become undefined. At this point, indent = indent || '';, indent is undefined which is "falsy". The or (||) used in assignments means that if the LHS is "falsy", continue to the RHS. (When the function is called the second time in the recursion indent = ' ', which is "truthy", meaning indent will still have that value.

Q11:

if (self.getMetaType(childNode) === childNode) {
    self.logger.info('childNode is a meta-node'); 
}

Q12:

var srcPath = self.core.getPointerPath(childNode, 'src');
var dstPath = self.core.getPointerPath(childNode, 'dst');

// Pathes are always non-empty strings (expect for the rootNode which
// cannot be the target of a pointer) and non-empty strings are "truthy"..
if (srcPath && dstPath) {
    var srcNode = nodes[srcPath];
    var dstNode = nodes[dstPath];
    self.logger.info(self.core.getAttribute(childNode, 'name'),
        'connects',
        self.core.getAttribute(srcNode, 'name'),
        '-->',
        self.core.getAttribute(dstNode, 'name'));
}

Q13: JSON stringfy

Q14: Expand the Details and they show up under Artifacts.

Q15: data is a complex blob-object that can hold multiple files, whereas metaNodeInfo.json is regular single file blob-object.