Source: plugin/PluginBase.js

/*globals define, requirejs*/
/*eslint-env node, browser*/

/**
 * This is the base class that plugins should inherit from.
 * (Using the PluginGenerator - the generated plugin will do that.)
 *
 * @author lattmann / https://github.com/lattmann
 * @author pmeijer / https://github.com/pmeijer
 */

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        define([
            'q',
            'plugin/PluginConfig',
            'plugin/PluginResultBase',
            'plugin/PluginResult',
            'plugin/InterPluginResult',
            'plugin/PluginMessage',
            'plugin/PluginNodeDescription',
            'plugin/util',
            'common/storage/constants'
        ], factory);
    } else if (typeof module === 'object' && module.exports) {
        module.exports = factory(
            require('q'),
            require('./PluginConfig'),
            require('./PluginResultBase'),
            require('./PluginResult'),
            require('./InterPluginResult'),
            require('./PluginMessage'),
            require('./PluginNodeDescription'),
            require('./util'),
            require('../common/storage/constants')
        );
    }
}(function (Q,
            PluginConfig,
            PluginResultBase,
            PluginResult,
            InterPluginResult,
            PluginMessage,
            PluginNodeDescription,
            pluginUtil,
            STORAGE_CONSTANTS) {
    'use strict';

    /**
     * Initializes a new instance of a plugin object, which should be a derived class.
     *
     * @constructor
     * @alias PluginBase
     */
    function PluginBase() {
        // set by initialize

        /**
         * @type {PluginMetadata}
         */
        this.pluginMetadata = null;

        /**
         * @type {GmeConfig}
         */
        this.gmeConfig = null;

        /**
         * @type {GmeLogger}
         */
        this.logger = null;

        /**
         * @type {BlobClient}
         */
        this.blobClient = null;

        this._currentConfig = null;

        // set by configure

        /**
         * @type {Core}
         */
        this.core = null;

        /**
         * @type {ProjectInterface}
         */
        this.project = null;

        this.projectName = null;
        this.projectId = null;
        this.branchName = null;

        this.branchHash = null;
        this.commitHash = null;
        this.currentHash = null;

        /**
         * @type {module:Core~Node}
         */
        this.rootNode = null;

        /**
         * @type {module:Core~Node}
         */
        this.activeNode = null;

        /**
         * @type {module:Core~Node[]}
         */
        this.activeSelection = [];

        /**
         * The namespace the META nodes are coming from (set by invoker).
         * The default is the full meta, i.e. the empty string namespace.
         * For example, if a project has a library A with a library B. The possible namespaces are:
         * '', 'A' and 'A.B'.
         * @type {string}
         */
        this.namespace = '';

        /**
         * The resolved META nodes based on the active namespace. Index by the fully qualified meta node names
         * with the namespace stripped off at the start.
         *
         * For example, if a project has a library A with a library B. If the project and the libraries all have
         * two meta nodes named a and b. Depending on the namespace the META will have the following keys:
         *
         * 1) namespace = '' -> ['a', 'b', 'A.a', 'A.b', 'A.B.a', 'A.B.b']
         * 2) namespace = 'A' -> ['a', 'b', 'B.a', 'B.b']
         * 3) namespace = 'A.B' -> ['a', 'b']
         *
         * (N.B. 'a' and 'b' in example 3) are pointing to the meta nodes defined in A.B.)
         *
         * @type {Object<string, module:Core~Node>}
         */
        this.META = null;

        /**
         * @type {PluginResultBase}
         */
        this.result = null;

        this.isConfigured = false;

        this.callDepth = 0;

        this.notificationHandlers = [];

        this.invokedPlugins = [];
    }

    //<editor-fold desc="Methods must be overridden by the derived classes">

    /**
     * Main function for the plugin to execute. This will perform the execution.
     * Notes:
     * <br>- Do NOT use console.log use this.logger.[error,warning,info,debug] instead.
     * <br>- Do NOT put any user interaction logic UI, etc. inside this function.
     * <br>- callback always have to be called even if error happened.
     *
     * @param {function} callback - the result callback
     * @param {null|Error} callback.err - status of the call
     * @param {PluginResult} callback.result - plugin result
     */
    PluginBase.prototype.main = function (/*callback*/) {
        throw new Error('implement this function in the derived class');
    };

    /**
     * Readable name of this plugin that can contain spaces.
     *
     * @returns {string}
     */
    PluginBase.prototype.getId = function () {
        if (this.pluginMetadata) {
            return this.pluginMetadata.id;
        } else {
            throw new Error('pluginMetadata is not defined - implement this function in the derived class');
        }
    };

    /**
     * Readable name of this plugin that can contain spaces.
     *
     * @returns {string}
     */
    PluginBase.prototype.getName = function () {
        if (this.pluginMetadata) {
            return this.pluginMetadata.name;
        } else {
            throw new Error('pluginMetadata is not defined - implement this function in the derived class');
        }
    };

    //</editor-fold>
    //<editor-fold desc="Methods could be overridden by the derived classes">

    /**
     * Current version of this plugin using semantic versioning.
     * @returns {string}
     */
    PluginBase.prototype.getVersion = function () {
        return this.pluginMetadata ? this.pluginMetadata.version : '0.1.0';
    };

    /**
     * A detailed description of this plugin and its purpose. It can be one or more sentences.
     *
     * @returns {string}
     */
    PluginBase.prototype.getDescription = function () {
        return this.pluginMetadata ? this.pluginMetadata.description : '';
    };

    /**
     * Configuration structure with names, descriptions, minimum, maximum values, default values and
     * type definitions.
     *
     * Example:
     *
     * [{
     *    "name": "logChildrenNames",
     *    "displayName": "Log Children Names",
     *    "description": '',
     *    "value": true, // this is the 'default config'
     *    "valueType": "boolean",
     *    "readOnly": false
     * },{
     *    "name": "logLevel",
     *    "displayName": "Logger level",
     *    "description": '',
     *    "value": "info",
     *    "valueType": "string",
     *    "valueItems": [
     *          "debug",
     *          "info",
     *          "warn",
     *          "error"
     *      ],
     *    "readOnly": false
     * },{
     *    "name": "maxChildrenToLog",
     *    "displayName": "Maximum children to log",
     *    "description": 'Set this parameter to blabla',
     *    "value": 4,
     *    "minValue": 1,
     *    "valueType": "number",
     *    "readOnly": false
     * }]
     *
     * @returns {object[]}
     */
    PluginBase.prototype.getConfigStructure = function () {
        return this.pluginMetadata ? this.pluginMetadata.configStructure : [];
    };
    //</editor-fold>
    //<editor-fold desc="Methods that can be used by the derived classes">

    /**
     * Updates the current success flag with a new value.
     *
     * NewValue = OldValue && Value
     *
     * @param {boolean} value - apply this flag on current success value
     * @param {string|null} message - optional detailed message
     */
    PluginBase.prototype.updateSuccess = function (value, message) {
        var prevSuccess = this.result.getSuccess();
        var newSuccessValue = prevSuccess && value;

        this.result.setSuccess(newSuccessValue);
        var msg = '';
        if (message) {
            msg = ' - ' + message;
        }

        this.logger.debug('Success was updated from ' + prevSuccess + ' to ' + newSuccessValue + msg);
    };

    /**
     * WebGME can export the META types as path and this method updates the generated domain specific types with
     * webgme node objects. These can be used to define the base class of new objects created through the webgme API.
     *
     * @param {object} generatedMETA
     */
    PluginBase.prototype.updateMETA = function (generatedMETA) {
        var name;
        for (name in this.META) {
            if (Object.hasOwn(this.META, name)) {
                generatedMETA[name] = this.META[name];
            }
        }

        // TODO: check if names are not the same
        // TODO: log if META is out of date
    };

    /**
     * Checks if the given node is of the given meta-type.
     * Usage: <tt>self.isMetaTypeOf(aNode, self.META['FCO']);</tt>
     * @param {module:Core~Node} node - Node to be checked for type.
     * @param {module:Core~Node} metaNode - Node object defining the meta type.
     * @returns {boolean} - True if the given object was of the META type.
     */
    PluginBase.prototype.isMetaTypeOf = function (node, metaNode) {
        if (metaNode) {
            return this.core.isTypeOf(node, metaNode);
        }

        return false;
    };

    /**
     * Finds and returns the node object defining the meta type for the given node.
     * @param {module:Core~Node} node - Node to be checked for type.
     * @returns {module:Core~Node} - Node object defining the meta type of node.
     */
    PluginBase.prototype.getMetaType = function (node) {
        return this.core.getMetaType(node);
    };

    /**
     * Returns true if node is a direct instance of a meta-type node (or a meta-type node itself).
     * @param {module:Core~Node} node - Node to be checked.
     * @returns {boolean}
     */
    PluginBase.prototype.baseIsMeta = function (node) {
        var self = this,
            baseName,
            namespace,
            baseNode = self.core.getBase(node);
        if (!baseNode) {
            // FCO does not have a base node, by definition function returns true.
            return true;
        }

        baseName = self.core.getAttribute(baseNode, 'name');
        namespace = self.core.getNamespace(baseNode).substr(self.namespace.length);

        if (namespace) {
            baseName = namespace + '.' + baseName;
        }

        return Object.hasOwn(self.META, baseName) &&
            self.core.getGuid(self.META[baseName]) === self.core.getGuid(baseNode);
    };

    /**
     * Gets the current configuration of the plugin that was set by the user and plugin manager.
     *
     * @returns {PluginConfig}
     */
    PluginBase.prototype.getCurrentConfig = function () {
        return this._currentConfig;
    };

    /**
     * Creates a new message for the user and adds it to the result.
     *
     * @param {module:Core~Node|object} node - webgme object which is related to the message
     * @param {string} message - feedback to the user
     * @param {string} severity - severity level of the message: 'debug', 'info' (default), 'warning', 'error'.
     */
    PluginBase.prototype.createMessage = function (node, message, severity) {
        var severityLevel = severity || 'info';

        var descriptor = new PluginNodeDescription({
            name: node ? this.core.getAttribute(node, 'name') : '',
            id: node ? this.core.getPath(node) : ''
        });
        var pluginMessage = new PluginMessage({
            commitHash: this.currentHash,
            activeNode: descriptor,
            message: message,
            severity: severityLevel
        });

        this.result.addMessage(pluginMessage);
    };

    /**
     * Sends a notification back to the invoker of the plugin, can be used to notify about progress.
     * @param {string|object} message - Message string or object containing message.
     * @param {string} message.message - If object it must contain a message.
     * @param {number} [message.progress] - Approximate progress (in %) of the plugin at time of sending.
     * @param {string} [message.severity='info'] - Severity level ('success', 'info', 'warn', 'error')
     * @param {boolean} [message.toBranch=false] - If true, and the plugin is running on the server on a branch -
     * will broadcast to all sockets in the branch room.
     * @param {function(Error)} [callback] - optional callback invoked when message has been emitted from server.
     * @param {null|Error} callback.err - status of the call
     */
    PluginBase.prototype.sendNotification = function (message, callback) {
        var self = this,
            cnt = self.notificationHandlers.length,
            notification = {},
            data = {
                type: STORAGE_CONSTANTS.PLUGIN_NOTIFICATION,
                notification: notification,
                projectId: self.projectId,
                branchName: self.branchName,
                pluginName: self.getName(),
                pluginId: self.getId(),
                pluginVersion: self.getVersion()
            };

        if (typeof message === 'string') {
            notification.message = message;
            notification.severity = notification.severity || 'info';
        } else {
            data.notification = message;
        }


        callback = callback || function (err) {
            if (err) {
                self.logger.error(err);
            }
        };

        function emitToHandlers() {
            if (cnt === 0) {
                callback(null);
                return;
            }
            cnt -= 1;
            self.notificationHandlers[cnt](data, function (err) {
                if (err) {
                    callback(err);
                } else {
                    emitToHandlers();
                }
            });
        }

        emitToHandlers();
    };

    /**
     * Saves all current changes if there is any to a new commit.
     * If the commit result is either 'FORKED' or 'CANCELED', it creates a new branch.
     *
     * N.B. This is a utility function for saving/persisting data. The plugin has access to the project and core
     * instances and may persist and make the commit as define its own behavior for e.g. 'FORKED' commits.
     * To report the commits in the PluginResult make sure to invoke this.addCommitToResult with the given status.
     *
     * @param {string|null} message - commit message
     * @param {function} [callback] - the result callback
     * @param {null|Error} callback.err - status of the call
     * @param {module:Storage~CommitResult} callback.commitResult - status of the commit made
     * @return {external:Promise} If no callback is given, the result will be provided in a promise
     */
    PluginBase.prototype.save = function (message, callback) {
        var self = this,
            persisted,
            commitMessage = '[Plugin] ' + self.getName() + ' (v' + self.getVersion() + ') updated the model.';

        commitMessage = message ? commitMessage + ' - ' + message : commitMessage;

        if (this.callDepth > 0) {
            self.logger.debug('Call-depth is greater than zero, will not persist "', this.callDepth, '"');
            self.result.addCommitMessage(commitMessage);
            return Q.resolve({
                hash: self.currentHash,
                // TODO: Do we need a status? Which one? SYNCED so it can proceed?
            }).nodeify(callback);
        }

        self.logger.debug('Saving project');
        persisted = self.core.persist(self.rootNode);
        if (Object.keys(persisted.objects).length === 0) {
            self.logger.debug('save invoked with no changes, will still proceed');
        }

        return self.project.makeCommit(self.branchName,
            [self.currentHash],
            persisted.rootHash,
            persisted.objects,
            commitMessage)
            .then(function (commitResult) {
                if (commitResult.status === STORAGE_CONSTANTS.SYNCED) {
                    self.currentHash = commitResult.hash;
                    self.logger.debug('"' + self.branchName + '" was updated to the new commit.');
                    self.addCommitToResult(STORAGE_CONSTANTS.SYNCED);
                    return commitResult;
                } else if (commitResult.status === STORAGE_CONSTANTS.FORKED) {
                    self.currentHash = commitResult.hash;
                    return self._createFork();
                } else if (commitResult.status === STORAGE_CONSTANTS.CANCELED) {
                    // Plugin running in the browser and the client has made changes since plugin was invoked.
                    // Since the commitData was never sent to the server, a commit w/o branch is made before forking.
                    return self.project.makeCommit(null,
                        [self.currentHash],
                        persisted.rootHash,
                        persisted.objects,
                        commitMessage)
                        .then(function (commitResult) {
                            self.currentHash = commitResult.hash; // This is needed in case hash is randomly generated.
                            return self._createFork();
                        });
                } else if (commitResult.status === STORAGE_CONSTANTS.MERGED) {
                    self.currentHash = commitResult.mergeHash;
                    self.addCommitToResult(STORAGE_CONSTANTS.MERGED);
                    // N.B. If the plugin makes multiple saves, it should fast-forward after a merged commit.
                    // Otherwise each new commit will have to be merge as well.
                    return commitResult;
                } else if (!self.branchName) {
                    self.currentHash = commitResult.hash;
                    self.addCommitToResult(commitResult.status);
                    return commitResult;
                } else {
                    throw new Error('setBranchHash returned unexpected status' + commitResult.status);
                }
            })
            .nodeify(callback);
    };

    PluginBase.prototype._createFork = function (callback) {
        // User can set self.forkName, but must make sure it is unique.
        var self = this,
            oldBranchName = self.branchName,
            forkName = self.forkName || self.branchName + '_' + Date.now();
        self.logger.warn('Plugin got forked from "' + self.branchName + '". ' +
            'Trying to create a new branch "' + forkName + '".');

        return self.project.createBranch(forkName, self.currentHash)
            .then(function (forkResult) {
                if (forkResult.status === STORAGE_CONSTANTS.SYNCED) {
                    self.branchName = forkName;
                    self.logger.debug('"' + self.branchName + '" was updated to the new commit.' +
                        '(Successive saves will try to save to this new branch.)');
                    self.addCommitToResult(STORAGE_CONSTANTS.FORKED);

                    return {status: STORAGE_CONSTANTS.FORKED, forkName: forkName, hash: forkResult.hash};
                } else if (forkResult.status === STORAGE_CONSTANTS.FORKED) {
                    self.branchName = null;
                    self.addCommitToResult(STORAGE_CONSTANTS.FORKED);

                    throw new Error('Plugin got forked from "' + oldBranchName + '". ' +
                        'And got forked from "' + forkName + '" too.');
                } else {
                    throw new Error('createBranch returned unexpected status' + forkResult.status);
                }
            })
            .nodeify(callback);
    };

    /**
     * If plugin is started from a branch - it will reload the instance's nodes and update the currentHash to
     * the current hash of the branch.
     *
     * N.B. Use this with caution, for instance manually referenced nodes in a plugin will still be part of the
     * previous commit. Additionally if the namespaces have changed between commits - the this.META might end up
     * being empty.
     * @param {function} [callback] - the result callback
     * @param {null|Error} callback.err - status of the call
     * @param {boolean} callback.didUpdate - true if there was a change and it updated the state to it
     * @return {external:Promise} If no callback is given, the result will be provided in a promise
     */
    PluginBase.prototype.fastForward = function (callback) {
        var self = this,
            options;

        if (this.callDepth > 0) {
            self.logger.warn('callDepth is greater than zero, will not fast-forward "', this.callDepth, '"');
            return Q.resolve(false).nodeify(callback);
        }

        return self.project.getBranchHash(self.branchName)
            .then(function (branchHash) {
                if (branchHash === '') {
                    throw new Error('Branch does not exist [' + self.branchName + ']');
                } else if (branchHash === self.currentHash) {
                    return false;
                } else {
                    options = {
                        activeNode: self.core.getPath(self.activeNode),
                        activeSelection: self.activeSelection.forEach(function (node) {
                            return self.core.getPath(node);
                        }),
                        namespace: self.namespace
                    };

                    return pluginUtil.loadNodesAtCommitHash(
                        self.project,
                        self.core,
                        branchHash,
                        self.logger,
                        options
                    );
                }
            })
            .then(function (result) {
                var didUpdate;

                if (result === false) {
                    didUpdate = false;
                } else {
                    self.currentHash = result.commitHash;
                    self.rootNode = result.rootNode;
                    self.activeNode = result.activeNode;
                    self.activeSelection = result.activeSelection;
                    self.META = result.META;
                    didUpdate = true;
                }

                return didUpdate;
            })
            .nodeify(callback);
    };

    /**
     * Adds the commit to the results. N.B. if you're using your own save method - make sure to update
     * this.currentHash and this.branchName accordingly before adding the commit.
     *
     * @param {string} status - Status of the commit 'SYNCED', 'FORKED', 'CANCELED', null.
     */
    PluginBase.prototype.addCommitToResult = function (status) {
        var newCommit = {
            commitHash: this.currentHash,
            branchName: this.branchName,
            status: status
        };

        this.result.addCommit(newCommit);
        this.logger.debug('newCommit added', newCommit);
    };

    /**
     * Checks if the activeNode has registered the plugin.
     *
     * @param {string} pluginId - Id of plugin
     * @returns {Error} - returns undefined if valid and an Error if not.
     */
    PluginBase.prototype.isInvalidActiveNode = function (pluginId) {
        var validPlugins = this.core.getRegistry(this.activeNode, 'validPlugins') || '';
        this.logger.debug('validPlugins for activeNode', validPlugins);

        if (validPlugins.split(' ').indexOf(pluginId) === -1) {
            return new Error('Plugin not registered as validPlugin for active node, validPlugins "' +
                validPlugins + '"');
        }
    };

    /**
     * Loads all the nodes in the subtree starting from node and returns a map from paths to nodes.
     * @param {module:Core~Node} [node=self.rootNode] - Optional node to preload nodes from,
     * by default all will be loaded.
     * @param {function} [callback] - the result callback
     * @param {null|Error} callback.err - status of the call
     * @param {object} callback.nodeMap - keys are paths and values are nodes
     * @return {external:Promise} If no callback is given, the result will be provided in a promise
     */
    PluginBase.prototype.loadNodeMap = function (node, callback) {
        var self = this;
        return self.core.loadSubTree(node || self.rootNode)
            .then(function (nodeArr) {
                var nodes = {},
                    i;

                for (i = 0; i < nodeArr.length; i += 1) {
                    nodes[self.core.getPath(nodeArr[i])] = nodeArr[i];
                }

                return nodes;
            })
            .nodeify(callback);
    };

    /**
     * Retrieves the identity of the current user of the opened project (the user who invoked the plugin).
     * @return {string} the userId
     */
    PluginBase.prototype.getUserId = function () {
        return this.project.getUserId();
    };

    /**
     * Initializes and invokes the given plugin (at pluginId).
     * Things to note:
     *  1. If the invoked plugin calls save - it will not persist nor make a commit. The message will be recorded in
     *  the InterPluginResult.
     *  2. Artifacts and files saved will be added to the blob-storage. Invoked plugins can expose the content by adding
     *  it to itself - the instance will be available in the InterPluginResult.
     *
     * @param {string} pluginId - Id of plugin that should be invoked
     * @param {object} [context] - Optional context for the invoked plugin
     * @param {object} [context.namespace=this.namespace] - Namespace (relative this.namespace)
     * @param {module:Core~Node} [context.activeNode=this.activeNode] - Active node of invoked plugin
     * @param {Array<module:Core~Node>} [context.activeSelection=this.activeSelection] - Active selection
     * of invoked plugin
     * @param {object} [context.pluginConfig] - Specific configuration parameters that should be used for the
     * invocation.
     * If not provided will first check if the currentConfig of this plugin contains this plugin as dependency within
     * the array this._currentConfig._dependencies. Finally it will fall back to the default config of the plugin.
     * @param {function} [callback] - the result callback
     * @param {null|Error} callback.err - status of the call
     * @param {InterPluginResult} callback.result - result from the invoked plugin
     * @return {external:Promise} If no callback is given, the result will be provided in a promise
     */
    PluginBase.prototype.invokePlugin = function (pluginId, context, callback) {
        var self = this,
            deferred = Q.defer(),
            pluginInstance;

        context = context || {};

        function getPluginClass() {
            var requireDeferred = Q.defer(),
                pluginPath = 'plugin/' + pluginId + '/' + pluginId + '/' + pluginId;

            requirejs([pluginPath],
                function (PluginClass) {
                    self.logger.debug('requirejs plugin from path: ' + pluginPath);
                    requireDeferred.resolve(PluginClass);
                },
                function (err) {
                    requireDeferred.reject(err);
                }
            );

            return requireDeferred.promise;
        }

        getPluginClass()
            .then(function (PluginClass) {
                var pluginConfig,
                    cfgKey;

                pluginInstance = new PluginClass();

                pluginInstance.initialize(self.logger.fork(pluginId), self.blobClient.getNewInstance(), self.gmeConfig);
                pluginInstance.result = new InterPluginResult(pluginInstance);

                ['core', 'project', 'branch', 'projectName', 'projectId', 'branchName', 'branchHash', 'commitHash',
                    'currentHash', 'rootNode', 'notificationHandlers']
                    .forEach(function (sameField) {
                        pluginInstance[sameField] = self[sameField];
                    });

                pluginInstance.activeNode = context.activeNode || self.activeNode;
                pluginInstance.activeSelection = context.activeSelection || self.activeSelection;

                if (context.namespace) {
                    pluginInstance.namespace = self.namespace === '' ?
                        context.namespace : self.namespace + '.' + context.namespace;

                    pluginInstance.META = pluginUtil
                        .getMetaNodesMap(pluginInstance.core,
                            pluginInstance.rootNode,
                            pluginInstance.logger,
                            pluginInstance.namespace);
                } else {
                    pluginInstance.namespace = self.namespace;
                    pluginInstance.META = self.META;
                }

                // Plugin config
                // 1. Get the default config for the plugin instance.
                pluginConfig = pluginInstance.getDefaultConfig();

                // 2. If the current-plugin has a sub-config for this plugin (from the default UI) - add those.
                if (Object.hasOwn(self._currentConfig, '_dependencies') &&
                    Object.hasOwn(self._currentConfig._dependencies, pluginId) &&
                    Object.hasOwn(self._currentConfig._dependencies[pluginId], 'pluginConfig')) {

                    for (cfgKey in self._currentConfig._dependencies[pluginId].pluginConfig) {
                        pluginConfig[cfgKey] = self._currentConfig._dependencies[pluginId].pluginConfig[cfgKey];
                    }
                }

                // 3. Finally use the specific config passed here.
                if (context.pluginConfig) {
                    for (cfgKey in context.pluginConfig) {
                        pluginConfig[cfgKey] = context.pluginConfig[cfgKey];
                    }
                }

                pluginInstance.setCurrentConfig(pluginConfig);
                pluginInstance.isConfigured = true;
                pluginInstance.callDepth = self.callDepth + 1;

                self.invokedPlugins.push(pluginInstance);
                return Q.ninvoke(pluginInstance, 'main');
            })
            .then(function (res) {
                var i;
                for (i = 0; i < self.invokedPlugins.length; i += 1) {
                    if (pluginInstance === self.invokedPlugins[i]) {
                        self.invokedPlugins.splice(i, 1);
                    }
                }
                deferred.resolve(res || pluginInstance.result);
            })
            .catch(function (err) {
                deferred.reject(err);
            });

        return deferred.promise.nodeify(callback);
    };

    /**
     * Adds a file to the blob storage and adds it to the plugin-result.
     * @param {string} name - file name.
     * @param {string|Buffer|ArrayBuffer} data - file content.
     * @param {function} [callback] - if provided no promise will be returned.
     * @param {null|Error} callback.err - status of the call
     * @param {string} callback.metadataHash - the "id" of the uploaded file
     * @return {external:Promise} If no callback is given, the result will be provided in a promise
     */
    PluginBase.prototype.addFile = function (name, content, callback) {
        var self = this;

        return this.blobClient.putFile(name, content)
            .then(function (metadataHash) {
                self.result.addArtifact(metadataHash);
                return metadataHash;
            })
            .nodeify(callback);
    };

    /**
     * Adds multiple files to the blob storage and bundles them as an artifact of which the hash is added to the
     * plugin-result.
     * @param {string} name - name of the file bundle.
     * @param {object.<string, string|Buffer|ArrayBuffer>} files - Keys are file names and values the content.
     * @param {function} [callback] - if provided no promise will be returned.
     * @param {null|Error} callback.err - status of the call.
     * @param {string} callback.metadataHash - the "id" of the uploaded artifact.
     * @return {external:Promise} If no callback is given, the result will be provided in a promise.
     */
    PluginBase.prototype.addArtifact = function (name, files, callback) {
        var self = this,
            artifact = this.blobClient.createArtifact(name);

        return artifact.addFilesAsSoftLinks(files)
            .then(function () {
                return artifact.save();
            })
            .then(function (metadataHash) {
                self.result.addArtifact(metadataHash);
                return metadataHash;
            })
            .nodeify(callback);
    };

    /**
     * Retrieves the file from blob storage.
     * @param {string} metadataHash - the "id" of the file to retrieve.
     * @param {null|Error} callback.err - status of the call.
     * @param {string} callback.content - the file content.
     * @return {external:Promise} If no callback is given, the result will be provided in a promise.
     */
    PluginBase.prototype.getFile = function (metadataHash, callback) {
        return this.blobClient.getObjectAsString(metadataHash).nodeify(callback);
    };

    /**
     * Retrieves the file from blob storage in binary format.
     * @param {string} metadataHash - the "id" of the file to retrieve.
     * @param {string} [subpath] - optional file-like path to sub-object if complex blob
     * @param {null|Error} callback.err - status of the call.
     * @param {Buffer} callback.content - the file content.
     * @return {external:Promise} If no callback is given, the result will be provided in a promise.
     */
    PluginBase.prototype.getBinFile = function (metadataHash, subpath, callback) {
        return this.blobClient.getObject(metadataHash, callback || null, subpath || null);
    };

    /**
     * Retrieves all the files in the artifact from the blob storage.
     * @param {string} metadataHash - the "id" of the artifact to retrieve.
     * @param {null|Error} callback.err - status of the call.
     * @param {object.<string, string>} callback.files - Keys are file names, and values the content.
     * @return {external:Promise} If no callback is given, the result will be provided in a promise.
     */
    PluginBase.prototype.getArtifact = function (metadataHash, callback) {
        var self = this,
            result = {};

        return this.blobClient.getMetadata(metadataHash)
            .then(function (metadata) {
                var promises = Object.keys(metadata.content)
                    .map(function (fileName) {
                        return self.blobClient.getObjectAsString(metadata.content[fileName].content)
                            .then(function (content) {
                                result[fileName] = content;
                            });
                    });

                return Q.all(promises);
            })
            .then(function () {
                return result;
            })
            .nodeify(callback);
    };


    //</editor-fold>
    //<editor-fold desc="Methods that are used by the Plugin Manager. Derived classes should not use these methods">

    /**
     * Initializes the plugin with objects that can be reused within the same plugin instance.
     *
     * @param {GmeLogger} logger - logging capability to console (or file) based on PluginManager configuration
     * @param {BlobClient} blobClient - virtual file system where files can be generated then saved as a zip file.
     * @param {GmeConfig} gmeConfig - global configuration for webGME.
     */
    PluginBase.prototype.initialize = function (logger, blobClient, gmeConfig) {
        if (logger) {
            this.logger = logger;
        } else {
            this.logger = console;
        }

        if (!gmeConfig) {
            // TODO: Remove this check at some point
            throw new Error('gmeConfig was not provided to Plugin.initialize!');
        }

        this.blobClient = blobClient;
        this.gmeConfig = gmeConfig;

        this._currentConfig = null;
        // initialize default configuration
        this.setCurrentConfig(this.getDefaultConfig());

        this.isConfigured = false;
    };

    /**
     * Configures this instance of the plugin for a specific execution. This function is called before the main by
     * the PluginManager.
     * Initializes the result with a new object.
     *
     * @param {object} config - specific context: project, branch, core, active object and active selection.
     */
    PluginBase.prototype.configure = function (config) {
        var self = this;
        this.core = config.core;
        this.project = config.project;
        this.branch = config.branch;  // This is only for client side.
        this.projectName = config.projectName;
        this.projectId = config.projectId;
        this.branchName = config.branchName;
        this.branchHash = config.branchName ? config.commitHash : null;

        this.commitHash = config.commitHash;
        this.currentHash = config.commitHash;

        this.rootNode = config.rootNode;
        this.activeNode = config.activeNode;
        this.activeSelection = config.activeSelection;

        this.namespace = config.namespace || '';

        this.META = this.META = config.META;

        this.result = new PluginResult();
        this.result.setProjectId(this.projectId);

        this.addCommitToResult(STORAGE_CONSTANTS.SYNCED);

        this.isConfigured = true;

        setTimeout(function () {
            self.sendNotification({
                toBranch: false,
                message: 'Plugin initialized.',
                progress: 0,
                type: STORAGE_CONSTANTS.PLUGIN_NOTIFICATION_TYPE.INITIATED
            });
        }, 0);
    };

    /**
     * Gets the default configuration based on the configuration structure for this plugin.
     *
     * @returns {PluginConfig}
     */
    PluginBase.prototype.getDefaultConfig = function () {
        var configStructure = this.getConfigStructure(),
            defaultConfig = new PluginConfig();

        for (var i = 0; i < configStructure.length; i += 1) {
            defaultConfig[configStructure[i].name] = configStructure[i].value;
        }

        return defaultConfig;
    };

    /**
     * Sets the current configuration of the plugin.
     *
     * @param {PluginConfig} newConfig - this is the actual configuration and NOT the configuration structure.
     */
    PluginBase.prototype.setCurrentConfig = function (newConfig) {
        this._currentConfig = newConfig;
    };

    /**
     * Gets the metadata for the plugin.
     *
     * @returns {PluginMetaData}
     */
    PluginBase.prototype.getMetadata = function () {
        return this.pluginMetadata;
    };

    /**
     * Gets the ids of the directly defined dependencies of the plugin
     *
     * @returns {string[]}
     */
    PluginBase.prototype.getPluginDependencies = function () {
        if (this.pluginMetadata && this.pluginMetadata.dependencies) {
            return this.pluginMetadata.dependencies
                .map(function (data) {
                    return data.id;
                });
        } else {
            return [];
        }
    };

    /**
     * Aborts the execution of a plugin.
     */
    PluginBase.prototype.onAbort = function () {
        throw new Error('onAbort function is not implemented!');
    };

    /**
     * Can send a message to the plugin.
     * @param {string} messageType - string identifier of the message.
     * @param {object} content - object that holds arbitrary content of the message.
     */
    PluginBase.prototype.onMessage = function (messageType, content) {
        if (this.logger) {
            this.logger.warn('Message [' + messageType + '] was received but no message handling is implemented!');
            this.logger.debug('Unhandled [' + messageType + '] with content:', content);
        }
    };
    //</editor-fold>

    return PluginBase;
}));