APIs

Show:
"use strict";
const assert = require("node-opcua-assert").assert;
const _ = require("underscore");
const chalk =require("chalk");

const NodeClass = require("node-opcua-data-model").NodeClass;
const QualifiedName = require("node-opcua-data-model").QualifiedName;
const NodeId = require("node-opcua-nodeid").NodeId;

const BaseNode = require("./base_node").BaseNode;
const ReferenceType = require("./referenceType").ReferenceType;
const UAVariable = require("./ua_variable").UAVariable;
const UAVariableType = require("./ua_variable_type").UAVariableType;
const UAObjectType = require("./ua_object_type").UAObjectType;
const UAObject = require("./ua_object").UAObject;
const UAMethod = require("./ua_method").UAMethod;
const UADataType = require("./ua_data_type").UADataType;
const Reference = require("./reference").Reference;
const View = require("./view").View;

const utils = require("node-opcua-utils");
const dumpIf = require("node-opcua-debug").dumpIf;

const BrowseDirection = require("node-opcua-data-model").BrowseDirection;

const coerceLocalizedText = require("node-opcua-data-model").coerceLocalizedText;
const makeNodeId = require("node-opcua-nodeid").makeNodeId;
const resolveNodeId = require("node-opcua-nodeid").resolveNodeId;

const doDebug = false;


const regExp1 = /^(s|i|b|g)=/;
const regExpNamespaceDotBrowseName  = /^[0-9]+:(.*)/;


/**
 *
 * @constructor
 * @params options {Object}
 * @params options.namespaceUri {string}
 * @params options.addressSpace {AddressSpace}
 * @params options.index {number}
 * @params options.version="" {string}
 * @params options.publicationDate="" {Date}
 *
 */
function UANamespace(options) {

    const self = this;
    assert(typeof options.namespaceUri === "string");
    assert(options.addressSpace.constructor.name === "AddressSpace");
    assert(typeof options.index === "number");

    self.namespaceUri = options.namespaceUri;
    self.addressSpace = options.addressSpace;
    self.index = options.index;
    self._nodeid_index = {};
    self._internal_id_counter = 1000;

    self._aliases = {};
    self._objectTypeMap = {};
    self._variableTypeMap = {};
    self._referenceTypeMap = {};
    self._referenceTypeMapInv = {};
    self._dataTypeMap = {};
}

UANamespace.prototype.getDefaultNamespace = function () {
    return (this.index === 0) ? this : this.addressSpace.getDefaultNamespace();
};

UANamespace.prototype.dispose = function () {
    const self = this;

    _.forEach(self._nodeid_index, function (node) {
        node.dispose();
    });
    self._nodeid_index = null;
    self.addressSpace = null;

    self._aliases = null;

    self._objectTypeMap = null;
    self._variableTypeMap = null;
    self._referenceTypeMap = null;
    self._referenceTypeMapInv = null;
    self._dataTypeMap = null;

};


UANamespace.prototype._build_new_NodeId = function () {
    let nodeId;
    do {
        nodeId = makeNodeId(this._internal_id_counter, this.index);
        this._internal_id_counter += 1;
    } while (this._nodeid_index.hasOwnProperty(nodeId));
    return nodeId;
};

UANamespace.prototype.findNode = function (nodeId) {
    if (typeof nodeId === "string") {
        if (nodeId.match(regExp1)) {
            nodeId = "ns=" + this.index+";"+ nodeId;
        }
    }
    nodeId = resolveNodeId(nodeId);
    assert(nodeId.namespace === this.index);
    return this._nodeid_index[nodeId.toString()];
};


function _adjust_options(self, options) {
    const ns = self.addressSpace.getNamespaceIndex(self.namespaceUri);
    if (!options.nodeId) {
        const id = self._getNextAvailableId();
        options.nodeId = new NodeId(NodeId.NodeIdType.NUMERIC, id, ns);
    }
    options.nodeId = NodeId.coerce(options.nodeId);
    if (typeof options.browseName === "string") {
        options.browseName = new QualifiedName({
            name: options.browseName,
            namespaceIndex: ns
        });
    }
    return options;
}


/**
 *
 * @param objectType {String}
 * @return {UAObjectType|null}
 */
UANamespace.prototype.findObjectType = function (objectType) {
    assert(typeof objectType === "string");
    return this._objectTypeMap[objectType];
};
/**
 *
 * @param variableType {String}
 * @returns {UAVariableType|null}
 */
UANamespace.prototype.findVariableType = function (variableType) {
    assert(typeof variableType === "string");
    return this._variableTypeMap[variableType];
};
/**
 *
 * @param dataType {String}
 * @returns {UADataType|null}
 */
UANamespace.prototype.findDataType = function (dataType) {
    assert(typeof dataType === "string");
    assert(this._dataTypeMap,"internal error : _dataTypeMap is missing");
    return this._dataTypeMap[dataType];
};
/**
 *
 * @param referenceType {String}
 * @returns  {ReferenceType|null}
 */
UANamespace.prototype.findReferenceType = function (referenceType) {
    assert(typeof referenceType === "string");
    return this._referenceTypeMap[referenceType];
};
/**
 * find a ReferenceType by its inverse name.
 * @method findReferenceTypeFromInverseName
 * @param inverseName {String} the inverse name of the ReferenceType to find
 * @return {ReferenceType}
 */
UANamespace.prototype.findReferenceTypeFromInverseName = function (inverseName) {
    assert(typeof inverseName === "string");
    const node = this._referenceTypeMapInv[inverseName];
    assert(!node || (node.nodeClass === NodeClass.ReferenceType && node.inverseName.text === inverseName));
    return node;
};

function _registerObjectType(self, node) {
    assert(self.index === node.nodeId.namespace);
    const key = node.browseName.name;
    assert(!self._objectTypeMap[key], " UAObjectType already declared");
    self._objectTypeMap[key] = node;
}

function _registerVariableType(self, node) {
    assert(self.index === node.nodeId.namespace);
    const key = node.browseName.name;
    assert(!self._variableTypeMap[key], " UAVariableType already declared");
    self._variableTypeMap[key] = node;
}

function _registerReferenceType(self, node) {
    assert(self.index === node.nodeId.namespace);
    assert(node.browseName instanceof QualifiedName);
    if (!node.inverseName) {
        // Inverse name is not required anymore in 1.0.4
        //xx console.log("Warning : node has no inverse Name ", node.nodeId.toString(), node.browseName.toString());
        node.inverseName = {text: node.browseName.name};
    }
    const key = node.browseName.name;

    assert(node.inverseName.text);
    assert(!self._referenceTypeMap[key], " Node already declared");
    assert(!self._referenceTypeMapInv[node.inverseName], " Node already declared");
    self._referenceTypeMap[key] = node;
    self._referenceTypeMapInv[node.inverseName.text] = node;
}

function _registerDataType(self, node) {
    assert(self.index === node.nodeId.namespace);
    const key = node.browseName.name;
    assert(node.browseName instanceof QualifiedName);
    assert(!self._dataTypeMap[key], " DataType already declared");
    self._dataTypeMap[key] = node;
}

UANamespace.prototype._register = function (node) {
    assert(node instanceof BaseNode, "Expecting a instance of BaseNode in _register");
    assert(node.nodeId instanceof NodeId, "Expecting a NodeId");
    if (node.nodeId.namespace !== this.index) {
        throw new Error("node must belongs to this namespace");
    }
    assert(node.nodeId.namespace === this.index && "node must belongs to this namespace");
    assert(node.hasOwnProperty("browseName"), "Node must have a browseName");
    //assert(node.browseName.namespaceIndex === this.index,"browseName must belongs to this namespace");

    const indexName = node.nodeId.toString();
    if (this._nodeid_index.hasOwnProperty(indexName)) {
        throw new Error("nodeId " + node.nodeId.displayText() + " already registered " + node.nodeId.toString());
    }

    this._nodeid_index[indexName] = node;

    if (node.nodeClass === NodeClass.ObjectType) {
        _registerObjectType(this, node);
    } else if (node.nodeClass === NodeClass.VariableType) {
        _registerVariableType(this, node);
    } else if (node.nodeClass === NodeClass.ReferenceType) {
        _registerReferenceType(this, node);
    } else if (node.nodeClass === NodeClass.DataType) {
        _registerDataType(this, node);
    } else if (node.nodeClass === NodeClass.Object) {
    } else if (node.nodeClass === NodeClass.Variable) {
    } else if (node.nodeClass === NodeClass.Method) {
    } else if (node.nodeClass === NodeClass.View) {
    } else {
        console.log("Invalid class Name", node.nodeClass);
        throw new Error("Invalid class name specified");
    }
};


function _unregisterObjectType() {
}

UANamespace.prototype._deleteNode = function (node) {

    const self = this;
    assert(node instanceof BaseNode);

    const indexName = node.nodeId.toString();
    // istanbul ignore next
    if (!self._nodeid_index.hasOwnProperty(indexName)) {
        throw new Error("deleteNode : nodeId " + node.nodeId.displayText() + " is not registered " + node.nodeId.toString());
    }
    if (node.nodeClass === NodeClass.ObjectType) {
        _unregisterObjectType(self, node);
    } else if (node.nodeClass === NodeClass.Object) {
    } else if (node.nodeClass === NodeClass.Variable) {
    } else if (node.nodeClass === NodeClass.Method) {
    } else if (node.nodeClass === NodeClass.View) {
    } else {
        console.log("Invalid class Name", node.nodeClass);
        throw new Error("Invalid class name specified");
    }
    assert(self._nodeid_index[indexName] === node);
    delete self._nodeid_index[indexName];

    node.dispose();
};
/**
 *
 * @method addAlias
 * @param alias_name {String} the alias name
 * @param nodeId {NodeId} NodeId must belong to this namespace
 */
UANamespace.prototype.addAlias = function (alias_name, nodeId) {
    assert(typeof alias_name === "string");
    assert(nodeId instanceof NodeId);
    assert(nodeId.namespace === this.index);
    this._aliases[alias_name] = nodeId;
};


const hasPropertyRefId = resolveNodeId("HasProperty");
const hasComponentRefId = resolveNodeId("HasComponent");
const sameNodeId = require("node-opcua-nodeid").sameNodeId;

function _identifyParentInReference(references) {
    assert(_.isArray(references));
    const candidates = references.filter(function (ref) {
        return ref.isForward === false &&
            (sameNodeId(ref.referenceType, hasComponentRefId) || sameNodeId(ref.referenceType, hasPropertyRefId));
    });
    assert(candidates.length <= 1);
    return candidates[0];
}

UANamespace.nodeIdNameSeparator = "-";

const _constructors_map = {
    "Object": UAObject,
    "ObjectType": UAObjectType,
    "ReferenceType": ReferenceType,
    "Variable": UAVariable,
    "VariableType": UAVariableType,
    "DataType": UADataType,
    "Method": UAMethod,
    "View": View
};

function __combineNodeId(parentNodeId, name) {
    let nodeId = null;
    if (parentNodeId.identifierType === NodeId.NodeIdType.STRING) {
        const childName = parentNodeId.value + UANamespace.nodeIdNameSeparator + name.toString();
        nodeId = new NodeId(NodeId.NodeIdType.STRING, childName, parentNodeId.namespace);
    }
    return nodeId;
}

UANamespace.prototype._construct_nodeId = function (options) {

    const self = this;
    const addressSpace = self.addressSpace;
    let nodeId = options.nodeId;

    if (!nodeId) {

        for (const ref of options.references) {
            ref._referenceType = addressSpace.findReferenceType(ref.referenceType);
            ref.referenceType = ref._referenceType.nodeId;
        }
        // find HasComponent, or has Property reverse
        const parentRef = _identifyParentInReference(options.references);
        if (parentRef) {
            assert(parentRef.nodeId instanceof NodeId);
            assert(options.browseName instanceof QualifiedName);
            nodeId = __combineNodeId(parentRef.nodeId, options.browseName);
        }
    } else if (typeof nodeId === "string") {
        if (nodeId.match(regExp1)) {
            nodeId = "ns="+ self.index + ";" + nodeId;
        }
    }
    nodeId = nodeId || self._build_new_NodeId();
    if (nodeId instanceof NodeId) {
        return nodeId;
    }
    nodeId = resolveNodeId(nodeId);
    assert(nodeId instanceof NodeId);
    return nodeId;
};

/**
 * @method _createNode
 * @private
 * @param options
 *
 * @param [options.nodeId==null]      {NodeId}
 * @param options.nodeClass  {NodeClass}
 * @param options.browseName {String|QualifiedName} the node browseName
 *    the browseName can be either a string : "Hello"
 *                                 a string with a namespace : "1:Hello"
 *                                 a QualifiedName : new QualifiedName({name:"Hello", namespaceIndex:1});
 * @param [options.displayName] {String|LocalizedText} the node display name
 * @param [options.description] {String|LocalizedText} the node description
 *
 * @return {BaseNode}
 * @private
 */
UANamespace.prototype._createNode = function (options) {

    const self = this;

    assert(options.nodeClass && options.nodeClass.key, " options.nodeClass must be specified");
    assert(options.browseName, "options.browseName must be specified");
    //xx assert(options.browseName instanceof QualifiedName ? (options.browseName.namespaceIndex === self.index): true,"Expecting browseName to have the same namepsaceIndex as the namespace");

    options.description = coerceLocalizedText(options.description);


    // browseName adjustment
    if (typeof options.browseName === "string") {

        const match = options.browseName.match(regExpNamespaceDotBrowseName);
        if (match) {
            const correctedName= match[1];
            // the application is using an old scheme
            console.log(chalk.green("Warning : since node-opcua 0.4.2 , namespace should not be prepended to the browse name anymore"));
            console.log("   ", options.browseName, " will be replaced with " , correctedName);
            console.log(" Please update your code");

            const indexVerif = parseInt(match[0]);
            if (indexVerif !== self.index) {
                console.log(chalk.red.bold("Error: namespace index used at the front of the browseName " + indexVerif + " do not match the index of the current namespace ("+ self.index+ ")"));
                console.log(" Please fix your code so that the created node is inserted in the correct namespace, please refer to the NodeOPCUA documentation");
            }
        }

        options.browseName = new QualifiedName({name: options.browseName, namespaceIndex: self.index});

    } else if (!(options.browseName instanceof QualifiedName)) {
        options.browseName = new QualifiedName(options.browseName);
    }
    assert(options.browseName instanceof QualifiedName, "Expecting options.browseName to be instanceof  QualifiedName ");

    // ------------- set display name
    if (!options.displayName) {
        assert(typeof(options.browseName.name) === "string" );
        options.displayName= options.browseName.name;
    }

    //--- nodeId adjustment
    options.nodeId = self._construct_nodeId(options);
    dumpIf(!options.nodeId, options); // missing node Id
    assert(options.nodeId instanceof NodeId);

    //assert(options.browseName.namespaceIndex === self.index,"Expecting browseName to have the same namepsaceIndex as the namespace");

    const Constructor = _constructors_map[options.nodeClass.key];

    if (!Constructor) {
        throw new Error(" missing constructor for NodeClass " + options.nodeClass.key);
    }

    options.addressSpace = self.addressSpace;
    const node = new Constructor(options);

    assert(node.nodeId);
    assert(node.nodeId instanceof NodeId);
    this._register(node);

    // object shall now be registered
    if (doDebug) {
        assert(_.isObject(this.findNode(node.nodeId)));
    }
    return node;
};


/**
 * @method _addVariable
 * @private
 */
UANamespace.prototype._addVariable = function (options) {

    const self = this;

    const addressSpace = self.addressSpace;
    const baseDataVariableTypeId = addressSpace.findVariableType("BaseDataVariableType").nodeId;

    assert(options.hasOwnProperty("browseName"), "options.browseName must be provided");
    assert(options.hasOwnProperty("dataType"), "options.dataType must be provided");

    options.historizing = !!options.historizing;

    // xx assert(self.FolderTypeId && self.BaseObjectTypeId); // is default address space generated.?

    // istanbul ignore next
    if (options.hasOwnProperty("hasTypeDefinition")) {
        throw new Error("hasTypeDefinition option is invalid. Do you mean typeDefinition instead ?");
    }
    // ------------------------------------------ TypeDefinition
    let typeDefinition = options.typeDefinition || baseDataVariableTypeId;
    typeDefinition = addressSpace._coerce_VariableTypeIds(typeDefinition);
    assert(typeDefinition instanceof NodeId);

    // ------------------------------------------ DataType
    options.dataType = addressSpace._coerce_DataType(options.dataType);

    options.valueRank = utils.isNullOrUndefined(options.valueRank) ? -1 : options.valueRank;
    assert(_.isFinite(options.valueRank));
    assert(typeof options.valueRank === "number");

    options.arrayDimensions = options.arrayDimensions || null;
    assert(_.isArray(options.arrayDimensions) || options.arrayDimensions === null);
    // -----------------------------------------------------


    options.minimumSamplingInterval = +options.minimumSamplingInterval || 0;
    let references = options.references || [];

    references = [].concat(references, [
        {referenceType: "HasTypeDefinition", isForward: true, nodeId: typeDefinition}
    ]);

    assert(!options.nodeClass || options.nodeClass === NodeClass.Variable);
    options.nodeClass = NodeClass.Variable;

    options.references = references;

    const variable = self.createNode(options);
    assert(variable instanceof UAVariable);
    return variable;
};

/**
 * add a variable as a component of the parent node
 *
 * @method addVariable
 * @param options
 * @param options.browseName {String} the variable name
 * @param options.dataType   {String} the variable datatype ( "Double", "UInt8" etc...)
 * @param [options.typeDefinition="BaseDataVariableType"]
 * @param [options.modellingRule=null] the Modelling rule : "Optional" , "Mandatory"
 * @param [options.valueRank= -1]   {Int} the valueRank
 * @param [options.arrayDimensions] {null| Array<Int>}
 * @return {Object}*
 */
UANamespace.prototype.addVariable = function (options) {

    const self = this;
    assert(arguments.length === 1, "Invalid arguments AddressSpace#addVariable now takes only one argument.");
    if (options.hasOwnProperty("propertyOf") && options.propertyOf) {
        assert(!options.typeDefinition || options.typeDefinition === "PropertyType");
        options.typeDefinition = options.typeDefinition || "PropertyType";

    } else {
        assert(!options.typeDefinition || options.typeDefinition !== "PropertyType");
    }
    return self._addVariable(options);
};


UANamespace.prototype._addObjectOrVariableType = function (options, topMostBaseType, nodeClass) {

    const self = this;
    const addressSpace = self.addressSpace;

    assert(typeof topMostBaseType === "string");
    assert(nodeClass === NodeClass.ObjectType || nodeClass === NodeClass.VariableType);

    assert(!options.nodeClass);
    assert(options.browseName);
    assert(typeof options.browseName === "string");

    const references = [];

    function process_subtypeOf_options(self, options, references) {

        // check common misspelling mistake
        assert(!options.subTypeOf, "misspell error : it should be 'subtypeOf' instead");
        if (options.hasOwnProperty("hasTypeDefinition")) {
            throw new Error("hasTypeDefinition option is invalid. Do you mean typeDefinition instead ?");
        }
        assert(!options.typeDefinition, " do you mean subtypeOf ?");

        const subtypeOfNodeId = addressSpace._coerceType(options.subtypeOf, topMostBaseType, nodeClass);

        assert(subtypeOfNodeId);
        references.push({referenceType: "HasSubtype", isForward: false, nodeId: subtypeOfNodeId});
    }

    process_subtypeOf_options(self, options, references);

    const objectType = this._createNode({
        browseName: options.browseName,
        displayName:   options.displayName,
        nodeClass: nodeClass,
        isAbstract: !!options.isAbstract,
        eventNotifier: +options.eventNotifier,
        references: references,
        nodeId: options.nodeId
    });

    objectType.propagate_back_references();

    objectType.install_extra_properties();

    objectType.installPostInstallFunc(options.postInstantiateFunc);

    return objectType;

};


/**
 * add a new Object type to the address space
 * @method addObjectType
 * @param options
 * @param options.browseName {String} the object type name
 * @param [options.displayName] {String|LocalizedText} the display name
 * @param [options.subtypeOf="BaseObjectType"] {String|NodeId|BaseNode} the base class
 * @param [options.nodeId] {String|NodeId} an optional nodeId for this objectType, if not provided a new nodeId will be created
 * @param [options.isAbstract = false] {Boolean}
 * @param [options.eventNotifier = 0] {Integer}
 * @param [options.postInstantiateFunc = null] {Function}
 *
 */
UANamespace.prototype.addObjectType = function (options) {
    const self = this;
    assert(!options.hasOwnProperty("dataType"), "an objectType should not have a dataType");
    assert(!options.hasOwnProperty("valueRank"), "an objectType should not have a valueRank");
    assert(!options.hasOwnProperty("arrayDimensions"), "an objectType should not have a arrayDimensions");
    return self._addObjectOrVariableType(options, "BaseObjectType", NodeClass.ObjectType);
};


/**
 * add a new Variable type to the address space
 * @method addVariableType
 * @param options
 * @param options.browseName {String} the object type name
 * @param [options.displayName] {String|LocalizedText} the display name
 * @param [options.subtypeOf="BaseVariableType"] {String|NodeId|BaseNode} the base class
 * @param [options.nodeId] {String|NodeId} an optional nodeId for this objectType, if not provided a new nodeId will be created
 * @param [options.isAbstract = false] {Boolean}
 * @param options.dataType {String|NodeId} the variable DataType
 * @param [options.valueRank = -1]
 * @param [options.arrayDimensions = null] { Array<Int>>
 *
 */

UANamespace.prototype.addVariableType = function (options) {

    const self = this;
    assert(!options.hasOwnProperty("arrayDimension"), "Do you mean ArrayDimensions ?");

    // dataType
    options.dataType = options.dataType || "Int32";
    options.dataType = self.addressSpace._coerce_DataType(options.dataType);

    // valueRank
    options.valueRank = utils.isNullOrUndefined(options.valueRank) ? -1 : options.valueRank;
    assert(_.isFinite(options.valueRank));
    assert(typeof options.valueRank === "number");

    // arrayDimensions
    options.arrayDimensions = options.arrayDimensions || null;
    assert(_.isArray(options.arrayDimensions) || options.arrayDimensions === null);

    const variableType = self._addObjectOrVariableType(options, "BaseVariableType", NodeClass.VariableType);

    variableType.dataType = options.dataType;
    variableType.valueRank = options.valueRank;
    variableType.arrayDimensions = options.arrayDimensions;

    return variableType;
};


UANamespace.prototype.addView = function (options) {

    const self = this;
    assert(arguments.length === 1, "Namespace#addView expecting a single argument");
    assert(options);
    assert(options.hasOwnProperty("browseName"));
    assert(options.hasOwnProperty("organizedBy"));
    const browseName = options.browseName;
    assert(typeof browseName === "string");

    const addressSpace = self.addressSpace;
    const baseDataVariableTypeId = addressSpace.findVariableType("BaseDataVariableType").nodeId;

    // ------------------------------------------ TypeDefinition
    const typeDefinition = options.typeDefinition || baseDataVariableTypeId;
    options.references = options.references || [];

    options.references.push({referenceType: "HasTypeDefinition", isForward: true, nodeId: typeDefinition});

    // xx assert(self.FolderTypeId && self.BaseObjectTypeId); // is default address space generated.?

    assert(!options.nodeClass);
    options.nodeClass = NodeClass.View;

    const view = self.createNode(options);
    assert(view instanceof View);
    assert(view.nodeId instanceof NodeId);
    assert(view.nodeClass === NodeClass.View);
    return view;
};


UANamespace.prototype.addObject = function (options) {

    assert(!options.nodeClass || options.nodeClass === NodeClass.Object);
    options.nodeClass = NodeClass.Object;

    const typeDefinition = options.typeDefinition || "BaseObjectType";
    options.references = options.references || [];
    options.references.push({referenceType: "HasTypeDefinition", isForward: true, nodeId: typeDefinition});
    options.eventNotifier = +options.eventNotifier;
    const obj = this.createNode(options);
    assert(obj instanceof UAObject);
    assert(obj.nodeClass === NodeClass.Object);
    return obj;
};


/**
 *
 * @method addFolder
 * @param parentFolder
 * @param options {String|Object}
 * @param options.browseName {String} the name of the folder
 * @param [options.nodeId] {NodeId}. An optional nodeId for this object
 *
 * @return {BaseNode}
 */
UANamespace.prototype.addFolder = function (parentFolder, options) {
    const self = this;
    if (typeof options === "string") {
        options = {browseName: options};
    }

    const addressSpace = self.addressSpace;

    assert(!options.typeDefinition, "addFolder does not expect typeDefinition to be defined ");
    const typeDefinition = addressSpace._coerceTypeDefinition("FolderType");
    parentFolder = addressSpace._coerceFolder(parentFolder);
    options.nodeClass = NodeClass.Object;
    options.references = [
        {referenceType: "HasTypeDefinition", isForward: true, nodeId: typeDefinition},
        {referenceType: "Organizes", isForward: false, nodeId: parentFolder.nodeId}
    ];
    const node = self.createNode(options);
    return node;
};

/**
 * @method addReferenceType
 * @param options
 * @param options.isAbstract
 * @param options.browseName
 * @param options.inverseName
 */
UANamespace.prototype.addReferenceType = function (options) {

    const namespace = this;
    const addressSpace = namespace.addressSpace;

    options.nodeClass = NodeClass.ReferenceType;
    options.references = options.references || [];


    if (options.subtypeOf) {
        assert(options.subtypeOf);
        const subtypeOfNodeId = addressSpace._coerceType(options.subtypeOf, "References", NodeClass.ReferenceType);
        assert(subtypeOfNodeId);

        console.log(subtypeOfNodeId.toString().cyan);
        options.references.push({referenceType: "HasSubtype", isForward: false, nodeId: subtypeOfNodeId});
    }
    const node = namespace._createNode(options);

    node.propagate_back_references();

    return node;
};

/**
 * @method createDataType
 * @param options
 * @param options.isAbstract
 * @param options.browseName {BrowseName}
 * @param options.superType {NodeId}
 * @param [options.nodeId]
 * @param [options.displayName]
 * @param [options.description]
 *
 */
UANamespace.prototype.createDataType = function (options) {
    assert(!options.hasOwnProperty("addressSpace"));
    assert(options.hasOwnProperty("isAbstract"));
    assert(!options.hasOwnProperty("nodeClass"));
    assert(options.hasOwnProperty("browseName"), "must provide a browseName");
    const self = this;
    options.nodeClass = NodeClass.DataType;
    options.references = options.references || [];

    if (options.references.length === 0) {
        assert(options.hasOwnProperty("superType"), "must provide a superType");

        options.superType = this.addressSpace.findDataType(options.superType);
        assert(options.superType);
        options.references.push({
            referenceType: "HasSubtype", isForward: false, nodeId: options.superType.nodeId
        });
    }
    const node = self._createNode(options);
    return node;
};


/**
 * @method _coerce_parent
 * convert a 'string' , NodeId or Object into a valid and existing object
 * @param addressSpace  {AddressSpace}
 * @param value
 * @param coerceFunc {Function}
 * @private
 */
function _coerce_parent(addressSpace, value, coerceFunc) {
    assert(_.isFunction(coerceFunc));
    if (value) {
        if (typeof value === "string") {
            value = coerceFunc.call(addressSpace, value);
        }
        if (value instanceof NodeId) {
            value = addressSpace.findNode(value);
        }
    }
    assert(!value || value instanceof BaseNode);
    return value;
}

function _handle_event_hierarchy_parent(addressSpace, references, options) {

    options.eventSourceOf = _coerce_parent(addressSpace, options.eventSourceOf, addressSpace._coerceNode);
    options.notifierOf = _coerce_parent(addressSpace, options.notifierOf, addressSpace._coerceNode);
    if (options.eventSourceOf) {
        assert(!options.notifierOf, "notifierOf shall not be provided with eventSourceOf ");
        references.push({referenceType: "HasEventSource", isForward: false, nodeId: options.eventSourceOf.nodeId});

    } else if (options.notifierOf) {
        assert(!options.eventSourceOf, "eventSourceOf shall not be provided with notifierOf ");
        references.push({referenceType: "HasNotifier", isForward: false, nodeId: options.notifierOf.nodeId});
    }
}

function _handle_hierarchy_parent(addressSpace, references, options) {

    options.componentOf = _coerce_parent(addressSpace, options.componentOf, addressSpace._coerceNode);
    options.propertyOf = _coerce_parent(addressSpace, options.propertyOf, addressSpace._coerceNode);
    options.organizedBy = _coerce_parent(addressSpace, options.organizedBy, addressSpace._coerceFolder);

    if (options.componentOf) {
        assert(!options.propertyOf);
        assert(!options.organizedBy);
        assert(addressSpace.rootFolder.objects, "addressSpace must have a rootFolder.objects folder");
        assert(options.componentOf.nodeId !== addressSpace.rootFolder.objects.nodeId, "Only Organizes References are used to relate Objects to the 'Objects' standard Object.");
        references.push({referenceType: "HasComponent", isForward: false, nodeId: options.componentOf.nodeId});
    }

    if (options.propertyOf) {
        assert(!options.componentOf);
        assert(!options.organizedBy);
        assert(options.propertyOf.nodeId !== addressSpace.rootFolder.objects.nodeId, "Only Organizes References are used to relate Objects to the 'Objects' standard Object.");
        references.push({referenceType: "HasProperty", isForward: false, nodeId: options.propertyOf.nodeId});
    }

    if (options.organizedBy) {
        assert(!options.propertyOf);
        assert(!options.componentOf);
        references.push({referenceType: "Organizes", isForward: false, nodeId: options.organizedBy.nodeId});
    }
}

UANamespace._handle_hierarchy_parent = _handle_hierarchy_parent;

function _copy_reference(reference) {
    assert(reference.hasOwnProperty("referenceType"));
    assert(reference.hasOwnProperty("isForward"));
    assert(reference.hasOwnProperty("nodeId"));
    assert(reference.nodeId instanceof NodeId);
    return {
        referenceType: reference.referenceType,
        isForward: reference.isForward,
        nodeId: reference.nodeId
    };
}

function _copy_references(references) {
    references = references || [];
    return references.map(_copy_reference);
}

function isNonEmptyQualifiedName(browseName) {
    if (!browseName) {
        return false;
    }
    if (typeof browseName === "string") {
        return browseName.length >= 0;
    }
    if (!(browseName instanceof QualifiedName)) {
        browseName = new QualifiedName(browseName);
    }
    assert(browseName instanceof QualifiedName);
    return browseName.name.length > 0;
}

UANamespace.isNonEmptyQualifiedName = isNonEmptyQualifiedName;

function _handle_node_version(node, options) {

    assert(options);
    if (options.nodeVersion) {
        assert(node.nodeClass === NodeClass.Variable || node.nodeClass === NodeClass.Object);

        const nodeVersion = node.addressSpace.getOwnNamespace().addVariable({
            propertyOf: node,
            browseName: "NodeVersion",
            dataType: "String"
        });
        const initialValue = _.isString(options.nodeVersion) ? options.nodeVersion : "0";
        //xx console.log(" init value =",initialValue);
        nodeVersion.setValueFromSource({dataType: "String", value: initialValue});
    }
}

const cetools = require("./address_space_change_event_tools");
const _handle_model_change_event = cetools._handle_model_change_event;


function isValidModellingRule(ruleName) {
    // let restrict to Mandatory or Optional for the time being
    return ruleName === null || ruleName === "Mandatory" || ruleName === "Optional";
}


/**
 * @method _process_modelling_rule
 * @param references {Array<Reference>} the array of references
 * @param modellingRule {Reference} the modellling Rule reference.
 * @private
 */
UANamespace._process_modelling_rule = function (references, modellingRule) {
    if (modellingRule) {
        assert(isValidModellingRule(modellingRule), "expecting a valid modelling rule");
        const modellingRuleName = "ModellingRule_" + modellingRule;
        //assert(self.findNode(modellingRuleName),"Modelling rule must exist");
        references.push({referenceType: "HasModellingRule", nodeId: modellingRuleName});
    }
};


/**
 * @method createNode
 * @param options
 * @param options.nodeClass
 * @param [options.nodeVersion {String} = "0" ] install nodeVersion
 * @param [options.modellingRule {String} = null]
 *
 */
UANamespace.prototype.createNode = function (options) {

    const self = this;

    let node = null;
    const addressSpace = self.addressSpace;

    addressSpace.modelChangeTransaction(function () {

        assert(isNonEmptyQualifiedName(options.browseName));
        //xx assert(options.hasOwnProperty("browseName") && options.browseName.length > 0);

        assert(options.hasOwnProperty("nodeClass"));
        options.references = addressSpace.normalizeReferenceTypes(options.references);

        const references = _copy_references(options.references);

        _handle_hierarchy_parent(addressSpace, references, options);

        _handle_event_hierarchy_parent(addressSpace, references, options);

        UANamespace._process_modelling_rule(references, options.modellingRule);

        options.references = references;

        node = self._createNode(options);
        assert(node.nodeId instanceof NodeId);

        node.propagate_back_references();

        node.install_extra_properties();

        _handle_node_version(node, options);

        _handle_model_change_event(node);

    });
    return node;
};
const _handle_delete_node_model_change_event = cetools._handle_delete_node_model_change_event;

/**
 * remove the specified Node from the address space
 *
 * @method deleteNode
 * @param  nodeOrNodeId
 *
 *
 */
UANamespace.prototype.deleteNode = function (nodeOrNodeId) {

    const self = this;
    let node = null;
    let nodeId;
    if (nodeOrNodeId instanceof NodeId) {
        nodeId = nodeOrNodeId;
        node = this.findNode(nodeId);
        // istanbul ignore next
        if (!node) {
            throw new Error(" deleteNode : cannot find node with nodeId" + nodeId.toString());
        }
    } else if (nodeOrNodeId instanceof BaseNode) {
        node = nodeOrNodeId;
        nodeId = node.nodeId;
    }
    if (nodeId.namespace !== self.index) {
        throw new Error("this node doesn't belong to this namespace");
    }

    const addressSpace = self.addressSpace;

    addressSpace.modelChangeTransaction(function () {

        // notify parent that node is being removed
        const hierarchicalReferences = node.findReferencesEx("HierarchicalReferences", BrowseDirection.Inverse);
        hierarchicalReferences.forEach(function (ref) {
            const parent = addressSpace.findNode(ref.nodeId);
            assert(parent);
            parent._on_child_removed(node);
        });

        function deleteNodePointedByReference(ref) {
            const addressSpace = self;
            const o = addressSpace.findNode(ref.nodeId);
            addressSpace.deleteNode(o.nodeId);
        }

        // recursively delete all nodes below in the hierarchy of nodes
        // TODO : a better idea would be to extract any references of type "HasChild"
        const components = node.findReferences("HasComponent", true);
        const properties = node.findReferences("HasProperty", true);

        // TODO: shall we delete nodes pointed by "Organizes" links here ?
        const subfolders = node.findReferences("Organizes", true);
        const rf = [].concat(components, properties, subfolders);

        rf.forEach(deleteNodePointedByReference);

        _handle_delete_node_model_change_event(node);

        node.unpropagate_back_references();

        // delete nodes from global index
        const namespace = addressSpace.getNamespace(node.nodeId.namespace);
        assert(namespace === self);
        namespace._deleteNode(node);
    });
};

exports.Namespace = UANamespace;