APIs

Show:
"use strict";
/* global Buffer */
/**
 * @module opcua.datamodel
 */

const Enum = require("node-opcua-enum");
const assert = require("node-opcua-assert").assert;
const isValidGuid = require("node-opcua-guid").isValidGuid;
const emptyGuid = require("node-opcua-guid").emptyGuid;
const _ = require("underscore");
/**
 * `NodeIdType` an enumeration that specifies the possible types of a `NodeId` value.
 * @class NodeIdType
 */
const NodeIdType = new Enum({
    /**
     * @static
     * @property NUMERIC
     * @type EnumItem
     * @default 0x1
     */
    NUMERIC: 0x01,
    /**
     * @static
     * @property STRING
     * @type EnumItem
     * @default 0x2
     */
    STRING: 0x02,
    /**
     * @static
     * @property GUID
     * @type EnumItem
     * @default 0x3
     */
    GUID: 0x03,
    /**
     * @static
     * @property BYTESTRING
     * @type EnumItem
     * @default 0x4
     */
    BYTESTRING: 0x04
});
exports.NodeIdType = NodeIdType;

/**
 * Construct a node ID
 *
 * @class NodeId
 * @param {NodeIdType}                identifierType   - the nodeID type
 * @param {Number|String|GUID|Buffer} value            - the node id value. The type of Value depends on identifierType.
 * @param {Number}                    namespace        - the index of the related namespace (optional , default value = 0 )
 * @example
 *
 *    ``` javascript
 *    const nodeId = new NodeId(NodeIdType.NUMERIC,123,1);
 *    ```
 * @constructor
 */
function NodeId(identifierType, value, namespace) {

    /**
     * @property identifierType
     * @type {NodeIdType}
     */
    this.identifierType = NodeIdType.get(identifierType.value);

    assert(this.identifierType);
    /**
     * @property  value
     * @type  {*}
     */

    this.value = value;
    /**
     * @property namespace
     * @type {Number}
     */
    this.namespace = namespace || 0;

    // namespace shall be a UInt16
    assert(this.namespace >= 0 && this.namespace <= 0xFFFF);

    assert(this.identifierType !== NodeIdType.NUMERIC || (this.value >= 0 && this.value <= 0xFFFFFFFF));
    assert(this.identifierType !== NodeIdType.GUID || isValidGuid(this.value));
    assert(this.identifierType !== NodeIdType.STRING || typeof this.value === "string");

}
NodeId.NodeIdType = NodeIdType;

/**
 * get the string representation of the nodeID.
 *
 * @method toString
 * @example
 *
 *    ``` javascript
 *    const nodeid = new NodeId(NodeIdType.NUMERIC, 123,1);
 *    console.log(nodeid.toString());
 *    ```
 *
 *    ```
 *    >"ns=1;i=123"
 *    ```
 *
 * @param [options.addressSpace] {AddressSpace}
 * @return {String}
 */
NodeId.prototype.toString = function (options) {

    const addressSpace = options ? options.addressSpace : null;
    let str;
    switch (this.identifierType) {
        case NodeIdType.NUMERIC:
            str = "ns=" + this.namespace + ";i=" + this.value;
            break;
        case NodeIdType.STRING:
            str = "ns=" + this.namespace + ";s=" + this.value;
            break;
        case NodeIdType.GUID:
            str = "ns=" + this.namespace + ";g=" + this.value;
            break;
        default:
            assert(this.identifierType === NodeIdType.BYTESTRING, "invalid identifierType in NodeId : " + this.identifierType);
            if (this.value)  {
                str = "ns=" + this.namespace + ";b=" + this.value.toString("hex");
            } else {
                str = "ns=" + this.namespace + ";b=<null>"
            }
            break;
    }

    if (addressSpace) {
        if (this.namespace === 0 && (this.identifierType === NodeIdType.NUMERIC)) {
            // find standard browse name
            const name = reverse_map(this.value) || "<undefined>";
            str += " " + name.green.bold;
        } else {
            // let use the provided address space to figure out the browseNode of this node.
            // to make the message a little bit more useful.
            const n = addressSpace.findNode(this);
            str += " " + (n ? n.browseName.toString().green : " (????)");
        }
    }
    return str;
};


/**
 * convert nodeId to a JSON string. same as {@link NodeId#toString }
 * @method  toJSON
 * @return {String}
 */
NodeId.prototype.toJSON = function () {
    return this.toString();
};


/**
 * @method isEmpty
 * @return {Boolean} true if the NodeId is null or empty
 */
NodeId.prototype.isEmpty = function () {
    switch (this.identifierType) {
        case NodeIdType.NUMERIC:
            return this.value === 0;
        case NodeIdType.STRING:
            return !this.value || this.value.length === 0;
        case NodeIdType.GUID:
            return !this.value || this.value === emptyGuid;
        default:
            assert(this.identifierType === NodeIdType.BYTESTRING, "invalid identifierType in NodeId : " + this.identifierType);
            return !this.value || this.value.length === 0;
    }
};

NodeId.NullNodeId = new NodeId(NodeIdType.NUMERIC,0);

exports.NodeId = NodeId;


const rege_ns_i = /ns=([0-9]+);i=([0-9]+)/;
const rege_ns_s = /ns=([0-9]+);s=(.*)/;
const rege_ns_b = /ns=([0-9]+);b=(.*)/;
const rege_ns_g = /ns=([0-9]+);g=(.*)/;


/**
 * Convert a value into a nodeId:
 * @class opcua
 * @method coerceNodeId
 * @static
 *
 * @description:
 *    - if nodeId is a string of form : "i=1234" => nodeId({ namespace: 0 , value=1234  , identifierType: NodeIdType.NUMERIC})
 *    - if nodeId is a string of form : "s=foo"  => nodeId({ namespace: 0 , value="foo" , identifierType: NodeIdType.STRING})
 *    - if nodeId is a {@link NodeId} :  coerceNodeId returns value
 *
 * @param value
 * @param namespace {Integer}
 */
function coerceNodeId(value, namespace) {

    let matches, two_first;

    if (value instanceof NodeId) {
        return value;
    }

    value = value || 0;
    namespace = namespace || 0;

    let identifierType = NodeIdType.NUMERIC;

    if (typeof value === "string") {
        identifierType = NodeIdType.STRING;

        two_first = value.substr(0, 2);
        if (two_first === "i=") {

            identifierType = NodeIdType.NUMERIC;
            value = parseInt(value.substr(2), 10);

        } else if (two_first === "s=") {

            identifierType = NodeIdType.STRING;
            value = value.substr(2);

        } else if (two_first === "b=") {

            identifierType = NodeIdType.BYTESTRING;
            value = Buffer.from(value.substr(2), "hex");

        } else if (two_first === "g=") {

            identifierType = NodeIdType.GUID;
            value = value.substr(2);

        } else if (isValidGuid(value)) {

            identifierType = NodeIdType.GUID;

        } else if ((matches = rege_ns_i.exec(value)) !== null) {
            identifierType = NodeIdType.NUMERIC;
            namespace = parseInt(matches[1], 10);
            value = parseInt(matches[2], 10);

        } else if ((matches = rege_ns_s.exec(value)) !== null) {

            identifierType = NodeIdType.STRING;
            namespace = parseInt(matches[1], 10);
            value = matches[2];

        } else if ((matches = rege_ns_b.exec(value)) !== null) {
            identifierType = NodeIdType.BYTESTRING;
            namespace = parseInt(matches[1], 10);
            value = Buffer.from(matches[2], "hex");

        } else if ((matches = rege_ns_g.exec(value)) !== null) {
            identifierType = NodeIdType.GUID;
            namespace = parseInt(matches[1], 10);
            value = matches[2];
        } else {
            throw new Error("String cannot be coerced to a nodeId : " + value);
        }

    } else if (value instanceof Buffer) {
        identifierType = NodeIdType.BYTESTRING;

    } else if (value instanceof Object) {

        // it could be a Enum or a NodeId Like object
        const tmp = value;
        value = tmp.value;
        namespace = namespace || tmp.namespace;
        identifierType = tmp.identifierType || identifierType;
        return new NodeId(identifierType, value, namespace);

    }
    return new NodeId(identifierType, value, namespace);
}
exports.coerceNodeId = coerceNodeId;


/**
 * construct a node Id from a value and a namespace.
 * @class opcua
 * @method makeNodeId
 * @static
 * @param {String|Buffer} value
 * @param [namespace]=0 {Number} the node id namespace
 * @return {NodeId}
 */
const makeNodeId = function makeNodeId(value, namespace) {

    value = value || 0;
    namespace = namespace || 0;

    let identifierType = NodeIdType.NUMERIC;
    if (typeof value === "string") {
        if (value.match(/^(s|g|b|i)=/)) {
            throw new Error("please use coerce NodeId instead");
        }
        //            1         2         3
        //  012345678901234567890123456789012345
        // "72962B91-FA75-4AE6-8D28-B404DC7DAF63"
        if (isValidGuid(value)) {
            identifierType = NodeIdType.GUID;
        } else {
            identifierType = NodeIdType.STRING;
            // detect accidental string of form "ns=x;x";
            assert(value.indexOf("ns=") === -1, " makeNodeId(string) ? did you mean using coerceNodeId instead? ");
        }
    } else if (value instanceof Buffer) {
        identifierType = NodeIdType.BYTESTRING;
    }

    const nodeId = new NodeId(identifierType, value, namespace);

    assert(nodeId.hasOwnProperty("identifierType"));

    return nodeId;
};

exports.makeNodeId = makeNodeId;

const constants = require("node-opcua-constants");

const DataTypeIds = constants.DataTypeIds;
const VariableIds = constants.VariableIds;
const ObjectIds = constants.ObjectIds;
const ObjectTypeIds = constants.ObjectTypeIds;
const VariableTypeIds = constants.VariableTypeIds;
const MethodIds = constants.MethodIds;
const ReferenceTypeIds = constants.ReferenceTypeIds;

// reverse maps
let _nodeid_to_name_index = {};
let _name_to_nodeid_index = {};

(function build_standard_nodeid_indexes() {

    function expand_map(direct_index) {
        for (const name in direct_index) {
            if (direct_index.hasOwnProperty(name)) {
                const value = direct_index[name];
                _nodeid_to_name_index[value] = name;
                _name_to_nodeid_index[name] = new NodeId(NodeIdType.NUMERIC, value, 0);
            }
        }
    }

    _nodeid_to_name_index = {};
    _name_to_nodeid_index = {};
    expand_map(ObjectIds);
    expand_map(ObjectTypeIds);
    expand_map(VariableIds);
    expand_map(VariableTypeIds);
    expand_map(MethodIds);
    expand_map(ReferenceTypeIds);
    expand_map(DataTypeIds);

})();

function reverse_map(nodeId) {
    return _nodeid_to_name_index[nodeId];
}


/**
 * @class opcua
 * @method resolveNodeId
 * @static
 * @param node_or_string {NodeId|String}
 * @return {NodeId}
 */
function resolveNodeId(node_or_string) {

    let nodeId;

    const raw_id = (typeof node_or_string === "string") ? _name_to_nodeid_index[node_or_string] : undefined;
    if (raw_id !== undefined) {
        return raw_id;
    } else {
        nodeId = coerceNodeId(node_or_string);
    }
    return nodeId;
}

exports.resolveNodeId = resolveNodeId;


/**
 * @class NodeId
 * @method displayText
 * @return {String}
 */
NodeId.prototype.displayText = function () {

    if (this.namespace === 0 && this.identifierType === NodeIdType.NUMERIC) {
        const name = reverse_map(this.value);
        if (name) {
            return name + " (" + this.toString() + ")";
        }
    }
    return this.toString();

};



function sameNodeId(n1,n2) {
    if (n1.identifierType.value !== n2.identifierType.value) {
        return false;
    }
    if (n1.namespace !== n2.namespace) {
        return false;
    }
    switch(n1.identifierType.value) {
        case NodeIdType.NUMERIC.value:
        case NodeIdType.STRING.value:
            return n1.value === n2.value;
        default:
            return _.isEqual(n1.value,n2.value);
    }
}
exports.sameNodeId = sameNodeId;