APIs

Show:
"use strict";
/* global describe,it,before*/

const assert = require("node-opcua-assert").assert;
const _ = require("underscore");

const Variant = require("node-opcua-variant").Variant;
const VariantArrayType = require("node-opcua-variant").VariantArrayType;
const DataType = require("node-opcua-variant").DataType;

const StatusCodes = require("node-opcua-status-code").StatusCodes;


const BaseNode = require("./base_node").BaseNode;
const UADataType = require("./ua_data_type").UADataType;
const UAObject = require("./ua_object").UAObject;
const UAVariable = require("./ua_variable").UAVariable;

const AddressSpace = require("./address_space").AddressSpace;

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

function makeStructure(dataType,bForce) {

    bForce = !!bForce;

    assert(dataType instanceof UADataType);

    const addressSpace = dataType.addressSpace;
    assert(addressSpace.constructor.name === "AddressSpace");
    assert(addressSpace instanceof AddressSpace);

    // istanbul ignore next
    if (!dataType.binaryEncodingNodeId) {
        throw new Error("DataType with name " + dataType.browseName.toString() + " has no binaryEncoding node\nplease check your nodeset file");
    }

    // if binaryEncodingNodeId is in the standard factory => no need to overwrite
    if (!bForce && (hasConstructor(dataType.binaryEncodingNodeId) ||   dataType.binaryEncodingNodeId.namespace === 0)) {
        //xx console.log("Skipping standard constructor".bgYellow ," for dataType" ,dataType.browseName.toString());
        return getConstructor(dataType.binaryEncodingNodeId);
    }
    // etc ..... please fix me
    const namespaceUri = addressSpace.getNamespaceUri(dataType.nodeId.namespace);
    return buildConstructorFromDefinition(addressSpace,dataType);
}

const resolveNodeId = require("node-opcua-nodeid").resolveNodeId;
const lowerFirstLetter = require("node-opcua-utils").lowerFirstLetter;

function _extensionobject_construct(options) {

    options = options || {};
    const dataType = this.constructor.dataType;
    const obj = this;

    for (const field of dataType.definition) {
        const fieldName =  field.$$name$$;
        obj[fieldName] = field.$$initialize$$(options[fieldName]);
    }
}
function initialize_Structure(field,options) {
    return new field.$$Constructor$$(options);
}

const util = require("util");
const _defaultTypeMap = require("node-opcua-factory/src/factories_builtin_types")._defaultTypeMap;
const ec = require("node-opcua-basic-types");
const encodeArray = ec.encodeArray;
const decodeArray = ec.decodeArray;

global._extensionobject_construct = _extensionobject_construct;

function _extensionobject_encode(stream) {

    BaseUAObject.prototype.encode.call(this,stream);
    const definition = this.constructor.dataType.definition;
    for(const field of definition) {
        const fieldName = field.$$name$$;
        field.$$func_encode$$.call(this[fieldName],stream);
    }
}

function struct_encode(stream) {
    this.encode(stream);
}
function struct_decode(stream) {
    this.decode(stream);
    return this;
}

function _extensionobject_decode(stream) {
    const definition = this.constructor.definition;
    assert(definition,"expected a definition for this class ");
    for(const field of definition) {
        const fieldName = field.$$name$$;
        this[fieldName] = field.$$func_decode$$.call(this[fieldName],stream);
    }
}

const getEnumeration = require("node-opcua-factory").getEnumeration;
const findBuiltInType  = require("node-opcua-factory").findBuiltInType;
/*
exports.registerBasicType = require("./src/factories_basic_type").registerBasicType;
exports.findSimpleType = require("./src/factories_builtin_types").findSimpleType;
exports.findBuiltInType  = require("./src/factories_builtin_types").findBuiltInType;
exports.registerBuiltInType = require("./src/factories_builtin_types").registerType;
*/
const BaseUAObject = require("node-opcua-factory").BaseUAObject;
const schema_helpers =  require("node-opcua-factory/src/factories_schema_helpers");

function initialize_array(func,options){
    options= options|| [];
    const result = options.map(function(element) {
        return func(element);
    });
    return result;
}

function encode_array(fun_encode_element,arr,stream) {
    stream.writeUInt32(arr.length);
    for (const el of arr) {
        fun_encode_element(el,stream);
    }
}
function decode_array(fun_decode_element,arr,stream) {
    const n = stream.readUInt32();
    if (n===0xFFFFFFFF) {
        return null;
    }
    const result =[];
    for (let i =0;i<n;i++) {
        result.push(fun_decode_element(stream));
    }
    return result;
}

function buildConstructorFromDefinition(addressSpace,dataType) {

    assert(dataType.definition && _.isArray(dataType.definition));
    const enumeration = addressSpace.findDataType("Enumeration");

    const className = dataType.browseName.name.replace("DataType","");

    assert(enumeration,"Enumeration Type not found: please check your nodeset file");
    const structure = addressSpace.findDataType("Structure");
    assert(structure,"Structure Type not found: please check your nodeset file");

    const Constructor  =new Function("options","_extensionobject_construct.apply(this,arguments);");
    assert(_.isFunction(Constructor));
    Object.defineProperty(Constructor, "name", { value: className });

    Constructor.definition = dataType.definition;
    Constructor.dataType = dataType;
    util.inherits(Constructor, BaseUAObject);
    Constructor.prototype.encode = _extensionobject_encode;
    Constructor.prototype.decode = _extensionobject_decode;


    for (const field of dataType.definition) {

        if (field.valueRank === 1) {
            field.$$name$$ = lowerFirstLetter(field.name.replace("ListOf",""));
        } else {
            field.$$name$$ = lowerFirstLetter(field.name);
        }
        const dataTypeId = resolveNodeId(field.dataType);
        const fieldDataType = addressSpace.findNode(dataTypeId);

        if (!fieldDataType) {
            throw new Error(" cannot find description for object " + dataTypeId +
                ". Check that this node exists in the nodeset.xml file");
        }
        // check if  dataType is an enumeration or a structure or  a basic type
        field.$$dataTypeId$$ = dataTypeId;
        field.$$dataType$$ = fieldDataType;
        field.$$isEnum$$ = false;
        field.$$isStructure$$ = false;
        if (fieldDataType.isSupertypeOf(enumeration)) {
            field.$$isEnum$$ = true;
            makeEnumeration(fieldDataType);
        } else if (fieldDataType.isSupertypeOf(structure)) {
            field.$$isStructure$$ = true;
            const FieldConstructor = makeStructure(fieldDataType);
            assert(_.isFunction(FieldConstructor));
            //xx field
            field.$$func_encode$$ = struct_encode;
            field.$$func_decode$$ = struct_decode;
            field.$$Constructor$$ = FieldConstructor;
            field.$$initialize$$  = initialize_Structure.bind(null,field);
        } else {
            const stuff = findBuiltInType(fieldDataType.browseName.name);
            field.$$func_encode$$ = stuff.encode;
            field.$$func_decode$$ = stuff.decode;
            assert(_.isFunction(field.$$func_encode$$));
            assert(_.isFunction(field.$$func_decode$$));
            field.schema = stuff;
            field.$$initialize$$ = schema_helpers.initialize_field.bind(null,field);
        }
        if (field.valueRank === 1) {
            field.$$initialize$$  = initialize_array.bind(null,field.$$initialize$$);
            field.$$func_encode$$ = encode_array.bind(null,field.$$func_encode$$);
            field.$$func_decode$$ = decode_array.bind(null,field.$$func_decode$$);

        }
    }

    // reconstruct _schema form
    const fields = [];
    for (const field of dataType.definition) {
        const data = {
            name: field.$$name$$,
            fieldType: field.$$dataType$$.browseName.name,
            isArray: (field.valueRank === 1)
        };
        if (field.$$isEnum$$) {
            data.category = "enumeration";
        } else if (field.$$isStructure$$) {
            data.category = "complex";
            data.fieldTypeConstructor = field.$$Constructor$$;
        } else {
            data.category = "basic"
        }
        fields.push(data);
    }

    Constructor.prototype._schema = {
        name: className,
        id: -1,
        fields: fields
    };


    return Constructor;
}
/*
 * define a complex Variable containing a array of extension objects
 * each element of the array is also accessible as a component variable.
 *
 */


/**
 *
 * create a node Variable that contains a array of ExtensionObject of a given type
 * @method createExtObjArrayNode
 * @param parentFolder
 * @param options
 * @param options.browseName
 * @param options.complexVariableType
 * @param options.variableType        the type of Extension objects stored in the array.
 * @param options.indexPropertyName
 * @return {Object|UAVariable}
 */
function createExtObjArrayNode(parentFolder, options) {

    assert(parentFolder instanceof UAObject);
    assert(typeof options.variableType === "string");
    assert(typeof options.indexPropertyName === "string");

    const addressSpace = parentFolder.addressSpace;
    const namespace = parentFolder.namespace;

    const complexVariableType = addressSpace.findVariableType(options.complexVariableType);
    assert(!complexVariableType.nodeId.isEmpty());


    const variableType = addressSpace.findVariableType(options.variableType);
    assert(!variableType.nodeId.isEmpty());

    const structure = addressSpace.findDataType("Structure");
    assert(structure, "Structure Type not found: please check your nodeset file");

    const dataType = addressSpace.findDataType(variableType.dataType);
    assert(dataType.isSupertypeOf(structure), "expecting a structure (= ExtensionObject) here ");


    const inner_options = {

        componentOf: parentFolder,

        browseName: options.browseName,
        dataType: dataType.nodeId,
        valueRank: 1,
        typeDefinition: complexVariableType.nodeId,
        value: {dataType: DataType.ExtensionObject, value: [], arrayType: VariantArrayType.Array}
    };

    const uaArrayVariableNode = namespace.addVariable(inner_options);
    assert(uaArrayVariableNode instanceof UAVariable);

    bindExtObjArrayNode(uaArrayVariableNode, options.variableType, options.indexPropertyName);

    return uaArrayVariableNode;

}

exports.createExtObjArrayNode = createExtObjArrayNode;


function getExtObjArrayNodeValue() {
    return new Variant({
        dataType: DataType.ExtensionObject,
        arrayType: VariantArrayType.Array,
        value: this.$$extensionObjectArray
    });
}

/**
 * @method prepareDataType
 * @private
 * @param dataType
 */
function prepareDataType(dataType) {
    assert(dataType instanceof UADataType);
    if (!dataType._extensionObjectConstructor) {
        dataType._extensionObjectConstructor = makeStructure(dataType);
        if (!dataType._extensionObjectConstructor) {
            console.warn("AddressSpace#constructExtensionObject : cannot make structure for " + dataType.toString());
        }
    }
}
exports.prepareDataType = prepareDataType;

/**
 * @method bindExtObjArrayNode
 * @param uaArrayVariableNode {UAVariable}
 * @param variableType        {DataType}
 * @param indexPropertyName   {String}
 * @return {UAVariable}
 */
function bindExtObjArrayNode(uaArrayVariableNode, variableType, indexPropertyName) {

    assert(uaArrayVariableNode instanceof UAVariable);
    assert(variableType);

    const addressSpace = uaArrayVariableNode.addressSpace;

    variableType = addressSpace.findVariableType(variableType);
    if (!variableType) {
        throw new Error("Cannot find VariableType " + variableType.toString());
    }
    assert(!variableType.nodeId.isEmpty());

    let structure = addressSpace.findDataType("Structure");
    assert(structure, "Structure Type not found: please check your nodeset file");

    let dataType = addressSpace.findDataType(variableType.dataType);
    assert(dataType.isSupertypeOf(structure), "expecting a structure (= ExtensionObject) here ");

    assert(!uaArrayVariableNode.$$variableType ,"uaArrayVariableNode has already been bound !");

    uaArrayVariableNode.$$variableType = variableType;

    structure = addressSpace.findDataType("Structure");
    assert(structure, "Structure Type not found: please check your nodeset file");

    // verify that an object with same doesn't already exist
    dataType = addressSpace.findDataType(variableType.dataType);
    assert(dataType.isSupertypeOf(structure), "expecting a structure (= ExtensionObject) here ");

    uaArrayVariableNode.$$dataType             = dataType;
    uaArrayVariableNode.$$extensionObjectArray = [];
    uaArrayVariableNode.$$indexPropertyName    = indexPropertyName;

    prepareDataType(dataType);

    uaArrayVariableNode.$$getElementBrowseName = function (extObj) {

        const indexPropertyName = this.$$indexPropertyName;

        if (!extObj.hasOwnProperty(indexPropertyName)) {
            console.log(" extension object do not have ", indexPropertyName, extObj);
        }
        //assert(extObj.constructor === addressSpace.constructExtensionObject(dataType));
        assert(extObj.hasOwnProperty(indexPropertyName));
        const browseName = extObj[indexPropertyName].toString();
        return browseName;
    };

    const options = {
        get: getExtObjArrayNodeValue,
        set: null // readonly
    };

    // bind the readonly
    uaArrayVariableNode.bindVariable(options,true);

    return uaArrayVariableNode;
}
exports.bindExtObjArrayNode = bindExtObjArrayNode;

/**
 * @method addElement
 * add a new element in a ExtensionObject Array variable
 * @param options {Object}   data used to construct the underlying ExtensionObject
 * @param uaArrayVariableNode {UAVariable}
 * @return {UAVariable}
 *
 * @method addElement
 * add a new element in a ExtensionObject Array variable
 * @param nodeVariable {UAVariable}   a variable already exposing an extension objects
 * @param uaArrayVariableNode {UAVariable}
 * @return {UAVariable}
 *
 * @method addElement
 * add a new element in a ExtensionObject Array variable
 * @param constructor {Function}  constructor of the extension object to create
 * @param uaArrayVariableNode {UAVariable}
 * @return {UAVariable}
 */

function addElement(options, uaArrayVariableNode) {

    assert(uaArrayVariableNode," must provide an UAVariable containing the array");
    assert(uaArrayVariableNode instanceof UAVariable,"expecting a UAVariable node here");
    // verify that arr has been created correctly
    assert(!!uaArrayVariableNode.$$variableType && !!uaArrayVariableNode.$$dataType,
            "did you create the array Node with createExtObjArrayNode ?");
    assert(uaArrayVariableNode.$$dataType instanceof UADataType);
    assert(uaArrayVariableNode.$$dataType._extensionObjectConstructor instanceof Function);

    const checkValue = uaArrayVariableNode.readValue();
    assert(checkValue.statusCode === StatusCodes.Good);
    assert(checkValue.value.dataType === DataType.ExtensionObject);

    const addressSpace = uaArrayVariableNode.addressSpace;

    let extensionObject = null;
    let elVar = null;
    let browseName;

    if (options instanceof UAVariable) {
        elVar = options;
        extensionObject = elVar.$extensionObject; // get shared extension object
        assert(extensionObject instanceof uaArrayVariableNode.$$dataType._extensionObjectConstructor,
            "the provided variable must expose a Extension Object of the expected type ");
        // add a reference
        uaArrayVariableNode.addReference({referenceType: "HasComponent", isFoward: true, nodeId: elVar.nodeId});
        //xx elVar.bindExtensionObject();

    } else {
        if (options instanceof uaArrayVariableNode.$$dataType._extensionObjectConstructor) {
            // extension object has already been created
            extensionObject = options;
        } else {
            extensionObject = addressSpace.constructExtensionObject(uaArrayVariableNode.$$dataType, options);
        }
        browseName = uaArrayVariableNode.$$getElementBrowseName(extensionObject);
        elVar = uaArrayVariableNode.$$variableType.instantiate({
            componentOf: uaArrayVariableNode.nodeId,
            browseName: browseName,
            value: {dataType: DataType.ExtensionObject, value: extensionObject}
        });
        elVar.bindExtensionObject();
        elVar.$extensionObject = extensionObject;
    }

    // also add the value inside
    uaArrayVariableNode.$$extensionObjectArray.push(extensionObject);

    return elVar;
}
exports.addElement = addElement;

function removeElementByIndex(uaArrayVariableNode, elementIndex) {

    const _array = uaArrayVariableNode.$$extensionObjectArray;

    assert(_.isNumber(elementIndex));

    const addressSpace = uaArrayVariableNode.addressSpace;
    const extObj = _array[elementIndex];
    const browseName = uaArrayVariableNode.$$getElementBrowseName(extObj);

    // remove element from global array (inefficient)
    uaArrayVariableNode.$$extensionObjectArray.splice(elementIndex, 1);

    // remove matching component
    const node = uaArrayVariableNode.getComponentByName(browseName);
    if (!node) {
        throw new Error(" cannot find component ");
    }

    // remove the hasComponent reference toward node
    uaArrayVariableNode.removeReference({referenceType: "HasComponent", isFoward: true, nodeId: node.nodeId});

    // now check if node has still some parent
    const parents = node.findReferencesEx("HasChild",BrowseDirection.Inverse);
    if (parents.length ===0){
        addressSpace.deleteNode(node.nodeId);
    }
}

/**
 *
 * @method removeElement
 * @param uaArrayVariableNode {UAVariable}
 * @param elementIndex {number}   index of element to remove in array
 *
 *
 * @method removeElement
 * @param uaArrayVariableNode {UAVariable}
 * @param element {UAVariable}   node of element to remove in array
 */
function removeElement(uaArrayVariableNode, element) {

    assert(element,"element must exist");
    const _array = uaArrayVariableNode.$$extensionObjectArray;
    if (_array.length === 0) {
        throw new Error(" cannot remove an element from an empty array ");
    }
    let elementIndex = -1;
    if (_.isNumber(element)) {
        elementIndex = element;
        assert(elementIndex >= 0 && elementIndex < _array.length);
    } else if (element instanceof BaseNode ) {
        // find element by name
        const browseNameToFind = element.browseName.name.toString();
        elementIndex = _array.findIndex(function (obj, i) {
            const browseName = uaArrayVariableNode.$$getElementBrowseName(obj).toString();
            return (browseName === browseNameToFind);
        });

    } else if (_.isFunction(element)) {
        elementIndex = _array.findIndex(element);
    }else{
        assert(_array[0].constructor.name === element.constructor.name,"element must match");
        elementIndex = _array.findIndex(function(x) { return x === element });
    }
    if (elementIndex < 0) {
        throw new Error(" cannot find element matching " + element.toString());
    }
    return removeElementByIndex(uaArrayVariableNode, elementIndex);
}
exports.removeElement = removeElement;