APIs

Show:
"use strict";

/*jslint bitwise: true */
/**
 * @module opcua.address_space
 */

require("object.values");
const util = require("util");
const utils = require("node-opcua-utils");

const EventEmitter = require("events").EventEmitter;

const NodeId = require("node-opcua-nodeid").NodeId;
const makeNodeId = require("node-opcua-nodeid").makeNodeId;
const resolveNodeId = require("node-opcua-nodeid").resolveNodeId;
const sameNodeId = require("node-opcua-nodeid").sameNodeId;

const coerceQualifyName = require("node-opcua-data-model").coerceQualifyName;
const QualifiedName = require("node-opcua-data-model").QualifiedName;
const coerceLocalizedText = require("node-opcua-data-model").coerceLocalizedText;
const AttributeNameById = require("node-opcua-data-model").AttributeNameById;
const ResultMask = require("node-opcua-data-model").ResultMask;
const NodeClass = require("node-opcua-data-model").NodeClass;
const makeNodeClassMask = require("node-opcua-data-model").makeNodeClassMask;
const AttributeIds = require("node-opcua-data-model").AttributeIds;
const BrowseDirection = require("node-opcua-data-model").BrowseDirection;
const ReferenceDescription = require("node-opcua-service-browse").ReferenceDescription;

const DataValue = require("node-opcua-data-value").DataValue;

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

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


exports.BrowseDirection = BrowseDirection;

const assert = require("node-opcua-assert").assert;
const _ = require("underscore");
const dumpIf = require("node-opcua-debug").dumpIf;
let ReferenceType = null;// will be defined after baseNode is defined

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

const doDebug = false;

const SessionContext = require("./session_context").SessionContext;
const Reference = require("./reference").Reference;


function defaultBrowseFilterFunc(session) {

    return true;
}

function _get_QualifiedBrowseName(browseName) {
    return coerceQualifyName(browseName);
}

/**
 * Base class for all address_space classes
 *
 * BaseNode is the base class for all the OPCUA objects in the address space
 * It provides attributes and a set of references to other nodes.
 * see:
 * {{#crossLink "UAObject"}}{{/crossLink}},
 * {{#crossLink "UAVariable"}}{{/crossLink}},
 * {{#crossLink "Reference"}}{{/crossLink}},
 * {{#crossLink "UAMethod"}}{{/crossLink}},
 * {{#crossLink "UAView"}}{{/crossLink}},
 * {{#crossLink "UAObjecType"}}{{/crossLink}},
 * {{#crossLink "UADataType"}}{{/crossLink}},
 * {{#crossLink "UAVariableType"}}{{/crossLink}},
 *
 * @class BaseNode
 * @constructor
 *
 * @param options
 * @param options.addressSpace {AddressSpace}
 * @param options.browseName {QualifiedName}
 * @param [options.displayName] {String|LocalizedText}
 * @param options.references {Reference[]}
 * @param [options.description]  {String|LocalizedText}
 * @param [options.browseFilter] {Function}
 *
 *
 *
 *
 */
function BaseNode(options) {

    const self = this;
    const _private = BaseNode_initPrivate(self);

    assert(this.nodeClass);
    assert(options.addressSpace); // expecting an address space
    assert(options.browseName instanceof QualifiedName, "Expecting a valid QualifiedName");
    assert(options.nodeId instanceof NodeId, "Expecting a valid NodeId");
    assert(options.addressSpace.constructor.name === "AddressSpace");
    options.references = options.references || [];

    _private.__address_space = options.addressSpace;

    this.nodeId = resolveNodeId(options.nodeId);

    // QualifiedName
    /**
     * the node browseName
     * @property browseName
     * @type QualifiedName
     * @static
     */
    this.browseName = _get_QualifiedBrowseName(options.browseName);

    // re-use browseName as displayName if displayName is missing
    options.displayName = options.displayName || options.browseName.name.toString();

    this._setDisplayName(options.displayName);


    this._setDescription(options.description);


    //Xx Object.defineProperty(this, "_cache",             {configurable: true,value:{}, hidden:true,enumerable: false});
    //xx Object.defineProperty(this, "_referenceIdx",      {configurable: true,value:{}, hidden:true,enumerable: false});
    //xx Object.defineProperty(this, "_back_referenceIdx", {configurable: true,value:{}, hidden:true,enumerable: false});


    // user defined filter function for browsing
    const _browseFilter = options.browseFilter || defaultBrowseFilterFunc;
    assert(_.isFunction(_browseFilter));
    Object.defineProperty(this, "_browseFilter", {
        configurable: true,
        value: _browseFilter,
        hidden: true,
        enumerable: false
    });

    // normalize reference type
    // this will convert any referenceType expressed with its inverseName into
    // its normal name and fix the isForward flag accordingly.
    // ( e.g "ComponentOf" isForward:true => "HasComponent", isForward:false)
    for (const reference of options.references) {
        self.__addReference(reference);
    }

}

util.inherits(BaseNode, EventEmitter);


const reservedNames = {
    "nodeClass": 0,
    //Xx "_cache":0,
    //Xx  "_referenceIdx":0,
    //Xx  "__back_referenceIdx":0,
    "__displayName": 0,
    "displayName": 0,
    "description": 0,
    "__description": 0,
    "typeDefinition": 0
};

BaseNode.Reference = Reference;


/**
 * @property displayName
 * @type LocalizedText[]
 */
Object.defineProperty(BaseNode.prototype, "__displayName", {writable: true, hidden: true, enumerable: false});
BaseNode.prototype._setDisplayName = function (displayName) {
    displayName = _.isArray(displayName) ? displayName : [displayName];
    const _displayName = displayName.map(coerceLocalizedText);
    Object.defineProperty(this, "__displayName", {
        configurable: true,
        value: _displayName,
        hidden: true,
        enumerable: false
    });
};
Object.defineProperty(BaseNode.prototype, "displayName", {

    get: function () {
        return this.__displayName;
    },
    set: function (value) {
        this._setDisplayName(value);
        /**
         * fires when the displayName is changed.
         * @event DisplayName_changed
         * @param dataValue {DataValue}
         */
        this._notifyAttributeChange(AttributeIds.DisplayName);
    },
    hidden: false,
    enumerable: true
});

BaseNode.prototype.getDisplayName = function (locale) {
    return this.__displayName[0].text;
};

/**
 * @property description
 * @type LocalizedText
 */
Object.defineProperty(BaseNode.prototype, "__description", {writable: true, hidden: true, enumerable: false});

BaseNode.prototype._setDescription = function (description) {
    const __description = coerceLocalizedText(description);
    Object.defineProperty(this, "__description", {
        configurable: true,
        value: __description,
        hidden: true,
        enumerable: false
    });
};

Object.defineProperty(BaseNode.prototype, "description", {

    get: function () {
        return this.__description;
    },
    set: function (value) {
        this._setDescription(value);
        /**
         * fires when the description attribute is changed.
         * @event Description_changed
         * @param dataValue {DataValue}
         */
        this._notifyAttributeChange(AttributeIds.Description);
    },
    hidden: false,
    enumerable: true
});

BaseNode.makeAttributeEventName = function (attributeId) {

    const attributeName = AttributeNameById[attributeId];
    return attributeName + "_changed";
};


BaseNode.prototype._notifyAttributeChange = function (attributeId) {
    const self = this;
    const event_name = BaseNode.makeAttributeEventName(attributeId);
    self.emit(event_name, self.readAttribute(SessionContext.defaultContext, attributeId));
};


function _is_valid_BrowseDirection(browseDirection) {
    return browseDirection === BrowseDirection.Forward ||
        browseDirection === BrowseDirection.Inverse ||
        browseDirection === BrowseDirection.Both
        ;
}

const g_weakMap = new WeakMap();

function BaseNode_initPrivate(self) {
    assert(self instanceof BaseNode);
    const _private = {
        _referenceIdx: {},
        _back_referenceIdx: {},
        __address_space: null,
        _cache: {}
    };
    g_weakMap.set(self, _private);
    return _private;
}

function BaseNode_getPrivate(self) {
    return g_weakMap.get(self);
}

BaseNode._getCache = function (self) {
    const _private = BaseNode_getPrivate(self);
    return _private._cache;
};


/**
 *
 * @param strReference
 * @param browseDirection
 * @returns {*}
 */
BaseNode.prototype.findReferencesEx = function (strReference, browseDirection) {

    browseDirection = browseDirection || BrowseDirection.Forward;
    assert(_is_valid_BrowseDirection(browseDirection));
    assert(browseDirection !== BrowseDirection.Both);

    let referenceType= null;
    if (typeof strReference === "string") {
        //xx strReference = strReference.browseName.toString();
        referenceType = this.addressSpace.findReferenceType(strReference);
        if (!referenceType) {
            throw new Error("Cannot resolve referenceType : "+ strReference);
        }
    } else {
        referenceType = strReference;
    }

    if (!referenceType) {
        // note: when loading nodeset2.xml files, reference type may not exit yet
        // throw new Error("expecting valid reference name " + strReference);
        return [];
    }

    assert(referenceType instanceof ReferenceType);
    assert(referenceType.nodeId instanceof NodeId);

    const self = this;
    const _private = BaseNode_getPrivate(self);

    const hash = "_refEx_" + referenceType.nodeId.toString() + browseDirection.toString();
    if (_private._cache[hash]) {
        return _private._cache[hash];
    }

    // find a map of all type that derives from the provided reference type
    const keys = referenceType.getSubtypeIndex();

    const isForward = (browseDirection === BrowseDirection.Forward);
    const references = [];

    function process(referenceIdx) {
        const referenceTypes = _.values(referenceIdx);
        for (let ref of referenceTypes) {
            const h = ref.referenceType.toString();
            if ( ref.isForward === isForward && keys[h] ) {
                assert(ref._referenceType instanceof ReferenceType);
                assert(ref._referenceType.browseName.toString());
                references.push(ref);
            }
        }
    }


    process(_private._referenceIdx);
    process(_private._back_referenceIdx);

    _private._cache[hash] = references;
    return references;
};

BaseNode.prototype.findReferencesExDescription = function (strReference, browseDirection) {

    const refs = this.findReferencesEx(strReference, browseDirection);
    const addressSpace = this.addressSpace;
    const r = refs.map(function (ref) {
        return _makeReferenceDescription(addressSpace, ref, 0x3F);
    });
    return r;
};


BaseNode.prototype._coerceReferenceType = function (referenceType) {

    const self = this;

    if (typeof referenceType === "string") {
        referenceType = self.addressSpace.findReferenceType(referenceType);
    } else if (referenceType instanceof NodeId) {
        referenceType = self.addressSpace.findNode(referenceType);
    }
    assert(referenceType instanceof ReferenceType);

    return referenceType;
};


/**
 * @method findReferences
 * @param referenceType {String|NodeId|ReferenceType} the referenceType as a string.
 * @param  [isForward=true] {Boolean}
 * @return {Array<Reference>}
 */
BaseNode.prototype.findReferences = function (referenceType, isForward) {

    const self = this;
    const _private = BaseNode_getPrivate(self);

    isForward = utils.isNullOrUndefined(isForward) ? true : !!isForward;
    assert(_.isBoolean(isForward));

    referenceType = this._coerceReferenceType(referenceType);

    const hash = "_ref_" + referenceType.nodeId.toString() + isForward.toString();
    if (_private._cache[hash]) {
        return _private._cache[hash];
    }

    // istanbul ignore next
    if (doDebug && !(this.addressSpace.findReferenceType(referenceType))) {
        throw new Error("expecting valid reference name " + referenceType);
    }

    const result = [];
    _.forEach(_private._referenceIdx, function (ref) {
        if (ref.isForward === isForward) {
            if (sameNodeId(ref.referenceType, referenceType.nodeId)) {
                result.push(ref);
            }
        }
    });

    _.forEach(_private._back_referenceIdx, function (ref) {
        if (ref.isForward === isForward) {
            if (sameNodeId(ref.referenceType, referenceType.nodeId)) {
                result.push(ref);
            }
        }
    });

    _private._cache[hash] = result;
    return result;
};


/**
 * @method findReference
 * @param strReference {String} the referenceType as a string.
 * @param [isForward] {Boolean|null}
 * @param [optionalSymbolicName] {String}
 * @return {Reference}
 */
BaseNode.prototype.findReference = function (strReference, isForward, optionalSymbolicName) {

    let refs = this.findReferences(strReference, isForward);

    if (optionalSymbolicName) {
        // search reference that matches symbolic name
        refs = refs.filter(function (ref) {
            return ref.symbolicName === optionalSymbolicName;
        });
    }
    assert(refs.length === 1 || refs.length === 0, "findReference: expecting only one or zero element here");
    return refs.length === 0 ? null : refs[0];
};


let displayWarning = true;


function toString_ReferenceDescription(ref, options) {

    const addressSpace = options.addressSpace;
    //xx assert(ref instanceof ReferenceDescription);
    const refNode = addressSpace.findNode(ref.referenceTypeId);
    if (!refNode) {
        return "Unknown Ref : " + ref;
    }
    const r = new Reference({
        referenceType: refNode.browseName.toString(),
        nodeId: ref.nodeId,
        isForward: ref.isForward
    });
    const str = r.toString(options);
    r.dispose();
    return str;
}

/* jshint latedef: false */
function _setup_parent_item(references) {

    references = _.map(references);

    /* jshint validthis: true */
    assert(this instanceof BaseNode);
    assert(_.isArray(references));
    const _private = BaseNode_getPrivate(this);

    assert(!_private.parent && "_setup_parent_item has been already called");

    const addressSpace = this.addressSpace;

    if (references.length > 0) {

        references = this.findReferencesEx("HasChild", BrowseDirection.Inverse);

        if (references.length >= 1) {
            // istanbul ignore next
            if (references.length > 1) {

                if (displayWarning) {

                    const options = {addressSpace: addressSpace};
                    console.warn("  More than one HasChild reference have been found for parent of object");
                    console.warn("    object node id:", this.nodeId.toString(), this.browseName.toString().cyan);
                    console.warn("    browseResults:");
                    console.warn(references.map(function (f) {
                        return toString_ReferenceDescription(f, options);
                    }).join("\n"));
                    console.warn("    first one will be used as parent");
                    //xx assert(browseResults.length === 1);
                    displayWarning = false;
                }
            }
            _private.parent = Reference._resolveReferenceNode(addressSpace, references[0]);
        }
    }
}


function _asObject(nodeIds, addressSpace) {
    function toObject(reference) {
        const obj = _resolveReferenceNode(addressSpace, reference);

        // istanbul ignore next
        if (false && !obj) {
            console.log(" Warning :  object with nodeId ".red + reference.nodeId.toString().cyan + " cannot be found in the address space !".red);
        }
        return obj;
    }

    function remove_null(o) {
        return !!o;
    }

    return nodeIds.map(toObject).filter(remove_null);
}

BaseNode.prototype.findReferencesExAsObject = function (strReference, browseDirection) {

    const nodeIds = this.findReferencesEx(strReference, browseDirection);
    const addressSpace = this.addressSpace;
    return _asObject(nodeIds, addressSpace);

};

BaseNode.prototype.findReferencesAsObject = function (strReference, isForward) {

    const nodeIds = this.findReferences(strReference, isForward);
    const addressSpace = this.addressSpace;
    return _asObject(nodeIds, addressSpace);
};


/**
 * returns the nodeId of this node's Type Definition
 * @property typeDefinition
 * @type {NodeId}
 */
BaseNode.prototype.__defineGetter__("typeDefinition", function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache.typeDefinition) {
        const has_type_definition_ref = this.findReference("HasTypeDefinition", true);
        _private._cache.typeDefinition = has_type_definition_ref ? has_type_definition_ref.nodeId : null;
    }
    return _private._cache.typeDefinition;
});


/**
 * returns the nodeId of this node's Type Definition
 * @property typeDefinitionObj
 * @type {BaseNode}
 */
BaseNode.prototype.__defineGetter__("typeDefinitionObj", function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (undefined === _private._cache.typeDefinitionObj) {
        const nodeId = this.typeDefinition;
        _private._cache.typeDefinitionObj = nodeId ? this.addressSpace.findNode(nodeId) : null;
    }
    return _private._cache.typeDefinitionObj;
});


/**
 * @method getAggregates
 * @return {BaseNode[]} return an array with the Aggregates of this object.
 */
BaseNode.prototype.getAggregates = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._aggregates) {
        _private._cache._aggregates = this.findReferencesExAsObject("Aggregates", BrowseDirection.Forward);
    }
    return _private._cache._aggregates;
};

/**
 * @method getComponents
 * @return {BaseNode[]} return an array with the components of this object.
 */
BaseNode.prototype.getComponents = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._components) {
        _private._cache._components = this.findReferencesExAsObject("HasComponent", BrowseDirection.Forward);
        //xx_private._cache._components = this.findReferencesAsObject("HasComponent", true);
    }
    return _private._cache._components;
};

/**
 * @method getProperties
 * @return {BaseNode[]} return a array with the properties of this object.
 */
BaseNode.prototype.getProperties = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._properties) {
        _private._cache._properties = this.findReferencesExAsObject("HasProperty", BrowseDirection.Forward);
    }
    return _private._cache._properties;
};

/**
 * @method getNotifiers
 * @return {BaseNode[]} return a array with the notifiers of this object.
 */
BaseNode.prototype.getNotifiers = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._notifiers) {
        _private._cache._notifiers = this.findReferencesAsObject("HasNotifier", true);
    }
    return _private._cache._notifiers;
};

/**
 * @method getEventSources
 * @return {BaseNode[]} return a array with the event source of this object.
 */
BaseNode.prototype.getEventSources = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._eventSources) {
        _private._cache._eventSources = this.findReferencesAsObject("HasEventSource", true);
    }
    return _private._cache._eventSources;
};

/**
 * @method getEventSourceOfs
 * @return {BaseNode[]} return a array of the objects for which this node is an EventSource
 */
BaseNode.prototype.getEventSourceOfs = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._eventSources) {
        _private._cache._eventSources = this.findReferencesAsObject("HasEventSource", false);
    }
    return _private._cache._eventSources;
};


/**
 * retrieve a component by name
 * @method getComponentByName
 * @param browseName
 * @param [namespaceIndex=null]
 * @return {BaseNode|null}
 */
BaseNode.prototype.getComponentByName = function (browseName, namespaceIndex) {
    assert(typeof browseName === "string");
    const components = this.getComponents();
    const select = _filter_by_browse_name(components, browseName, namespaceIndex);
    assert(select.length <= 1, "BaseNode#getComponentByName found duplicated reference");
    return select.length === 1 ? select[0] : null;
};
/**
 * retrieve a property by name
 * @method getPropertyByName
 * @param browseName
 * @param [namespaceIndex=null]
 * @return {BaseNode|null}
 */
BaseNode.prototype.getPropertyByName = function (browseName, namespaceIndex) {
    assert(typeof browseName === "string");
    const properties = this.getProperties();
    const select = _filter_by_browse_name(properties, browseName, namespaceIndex);
    assert(select.length <= 1, "BaseNode#getPropertyByName found duplicated reference");
    return select.length === 1 ? select[0] : null;
};

/**
 * retrieve a folder by name
 * @method getPropertyByName
 * @param browseName
 * @param [namespaceIndex=null]
 * @return {BaseNode|null}
 */
BaseNode.prototype.getFolderElementByName = function (browseName, namespaceIndex) {
    assert(typeof browseName === "string");
    const elements = this.getFolderElements();
    const select = _filter_by_browse_name(elements, browseName, namespaceIndex);
    return select.length === 1 ? select[0] : null;
};

/**
 * returns the list of nodes that this folder object organizes
 * @method getFolderElements
 * @return {Array<UAObject>}
 *
 */
BaseNode.prototype.getFolderElements = function () {
    return this.findReferencesAsObject("Organizes", true);
};

/**
 * returns the list of methods that this object provides
 * @method getMethods
 * @return {Array<UAObject>} returns an array wit"h Method objects.
 *
 *
 * Note: internally, methods are special types of components
 */
BaseNode.prototype.getMethods = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._methods) {
        const components = this.getComponents();
        const UAMethod = require("./ua_method").UAMethod;
        _private._cache._methods = components.filter(function (obj) {
            return obj instanceof UAMethod;
        });
    }
    return _private._cache._methods;
};

/**
 * returns true if the object has some opcua methods
 * @property hasMethods
 * @type {Boolean}
 */
BaseNode.prototype.__defineGetter__("hasMethods", function () {
    return this.getMethods().length > 0;
});

/**
 * @method getMethodById
 * @param nodeId
 * @return {UAMethod|null}
 */
BaseNode.prototype.getMethodById = function (nodeId) {

    const methods = this.getMethods();
    return _.find(methods, function (m) {
        return m.nodeId.toString() === nodeId.toString();
    });
};

function _filter_by_browse_name(components, browseName, namespaceIndex) {
    let select = [];
    if (namespaceIndex === null || namespaceIndex === undefined) {

        select = components.filter(function (c) {
            return c.browseName.name.toString() === browseName;
        });
    } else {
        select = components.filter(function (c) {
            return c.browseName.name.toString() === browseName && c.browseName.namespaceIndex === namespaceIndex;
        });
    }
    return select;
}

/**
 * @method getMethodByName
 * @param browseName
 * @param [namespaceIndex=null]
 * @return {UAMethod|null}
 */
BaseNode.prototype.getMethodByName = function (browseName, namespaceIndex) {
    assert(typeof browseName === "string");
    const methods = this.getMethods();
    const select = _filter_by_browse_name(methods, browseName, namespaceIndex);
    assert(select.length <= 1, "BaseNode#getMethodByName found duplicated reference");
    return select.length === 1 ? select[0] : null;
};

/**
 * returns the nodeId of the Type which is the super type of this
 * @property subtypeOf
 * @type {NodeId}
 */
BaseNode.prototype.__defineGetter__("subtypeOf", function subtypeOf() {
    return this.subtypeOfObj ? this.subtypeOfObj.nodeId : null;
});

BaseNode.prototype.__defineGetter__("subtypeOfObj", function subtypeOfObj() {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache._subtypeOfObj) {
        const is_subtype_of_ref = this.findReference("HasSubtype", false);
        if (is_subtype_of_ref) {
            _private._cache._subtypeOfObj = Reference._resolveReferenceNode(this.addressSpace, is_subtype_of_ref);
        }
    }
    return _private._cache._subtypeOfObj;
});


BaseNode.prototype.__findReferenceWithBrowseName = function (referenceType, browseName) {
    const refs = this.findReferencesAsObject(referenceType);

    function hasBrowseName(node) {
        return node.browseName.toString() === browseName;
    }

    const ref = refs.filter(hasBrowseName)[0];
    return ref;
};


/**
 * @property namespaceIndex
 * @type {Number}
 */
BaseNode.prototype.__defineGetter__("namespaceIndex", function () {
    return this.nodeId.namespace;
});

/**
 * @property namespaceUri
 * @type {String}
 */
BaseNode.prototype.__defineGetter__("namespaceUri", function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (!_private._cache.namespaceUri) {
        _private._cache.namespaceUri = this.addressSpace.getNamespaceUri(this.namespaceIndex);
    }
    return _private._cache.namespaceUri;
});

/**
 * the parent node
 * @property parent
 * @type {BaseNode}
 */
BaseNode.prototype.__defineGetter__("parent", function () {

    const self = this;
    const _private = BaseNode_getPrivate(self);
    if (_private.parent === undefined) {
        _setup_parent_item.call(this, _private._referenceIdx);
    }
    return _private.parent;
});

/**
 * @method resolveNodeId
 * @param nodeId
 * @return {NodeId}
 */
BaseNode.prototype.resolveNodeId = function (nodeId) {
    return this.addressSpace.resolveNodeId(nodeId);
};

BaseNode.prototype._remove_backward_reference = function (reference) {
    const self = this;
    const _private = BaseNode_getPrivate(self);

    assert(reference instanceof Reference);

    _remove_HierarchicalReference(self, reference);
    const h = reference.hash;

    if (_private._back_referenceIdx[h]) {
        // note : h may not exist in _back_referenceIdx since we are not indexing
        //        _back_referenceIdx to UAObjectType and UAVariableType for performance reasons
        _private._back_referenceIdx[h].dispose();
        delete _private._back_referenceIdx[h];
    }
    reference.dispose();
};

BaseNode.prototype._add_backward_reference = function (reference) {

    const self = this;
    const _private = BaseNode_getPrivate(self);

    assert(reference instanceof Reference);
    //xx assert(Reference.is_valid_reference(reference));

    const h = reference.hash;
    assert(_.isString(h));
    // istanbul ignore next
    if (_private._referenceIdx[h]) {
        //  the reference exists already in the forward references
        //  this append for instance when the XML NotSetFile has redundant <Reference>
        //  in this case there is nothing to do
        return;
    }
    // istanbul ignore next
    if (_private._back_referenceIdx[h]) {
        const opts = {addressSpace: self.addressSpace};
        console.warn(" Warning !", self.browseName.toString());
        console.warn("    ", reference.toString(opts));
        console.warn(" already found in ===>");
        console.warn(_.map(_private._back_referenceIdx, c => c.toString(opts)).join("\n"));
        console.warn("===>");
        throw new Error("reference exists already in _back_references");
    }

    if (!reference._referenceType) {
        var w = 1;
    }
    assert(reference._referenceType instanceof ReferenceType);

    _private._back_referenceIdx[h] = reference;
    _handle_HierarchicalReference(self, reference);
    self._clear_caches();

};

let displayWarningReferencePointingToItsef = true;

function _is_massively_used_reference(referenceType) {
    const name = referenceType.browseName.toString();
    return name === "HasTypeDefinition" || name === "HasModellingRule";

}

function _propagate_ref(self, addressSpace, reference) {

    // filter out non  Hierarchical References
    const referenceType = _resolveReferenceType(addressSpace, reference);

    // istanbul ignore next
    if (!referenceType) {
        console.error(" ERROR".red, " cannot find reference ", reference.referenceType, reference.toString());
    }

    // ------------------------------- Filter out back reference when reference type
    //                                 is HasTypeDefinition, HasModellingRule, etc ...
    //
    // var referenceNode = Reference._resolveReferenceNode(addressSpace,reference);
    // ignore propagation on back reference to UAVariableType or UAObject Type reference
    // because there are too many !
    if (!referenceType || _is_massively_used_reference(referenceType)) {
        //xx &&(referenceNode.constructor.name === "UAVariableType" || referenceNode.constructor.name  === "UAObjectType")
        // console.log(referenceType.browseName.toString() ,referenceNode.browseName.toString(), "on ",self.browseName.toString());
        return;
    }
    // ------------------------------- EXPERIMENT


    //xx if (!referenceType.isSupertypeOf(hierarchicalReferencesId)) { return; }
    const related_node = _resolveReferenceNode(addressSpace, reference);
    if (related_node) {

        // verify that reference doesn't point to object itself (see mantis 3099)
        if (sameNodeId(reference.nodeId, self.nodeId)) {

            // istanbul ignore next
            if (displayWarningReferencePointingToItsef) {
                // this could happen with method
                console.warn("  Warning: a Reference is pointing to itself ", self.nodeId.toString(), self.browseName.toString());
                displayWarningReferencePointingToItsef = false;
            }

        }
        //xx ignore this assert(reference.nodeId.toString() !== self.nodeId.toString());
        //function w(s,l) { return (s+"                                                          ").substr(0,l);}
        //if (reference.isForward) {
        //    console.log("  CHILD => ",w(related_node.browseName   + " " + related_node.nodeId.toString(),30),
        //        "  PARENT   ",w(self.browseName + " " + self.nodeId.toString(),30) , reference.toString());
        //} else {
        //    console.log("  CHILD => ",w(self.browseName   + " " + self.nodeId.toString(),30),
        //        "  PARENT   ",w(related_node.browseName + " " + related_node.nodeId.toString(),30) , reference.toString());
        //
        //}
        related_node._add_backward_reference(new Reference({
            referenceType: reference.referenceType,
            _referenceType: reference._referenceType,
            isForward: !reference.isForward,
            nodeId: self.nodeId,
            node:self
        }));
    } // else addressSpace may be incomplete and under construction (while loading a nodeset.xml file for instance)
}

/**
 * this methods propagates the forward references to the pointed node
 * by inserting backward references to the counter part node
 *
 * @method propagate_back_references
 */
BaseNode.prototype.propagate_back_references = function () {

    const self = this;
    const _private = BaseNode_getPrivate(self);

    if (self.addressSpace.suspendBackReference) {

        // this indicates that the base node is constructed from an xml definition
        // propagate_back_references will be called later once the file has been completely processed.
        return;
    }
    const addressSpace = self.addressSpace;
    for (let reference of _.values(_private._referenceIdx)) {
        _propagate_ref(self, addressSpace, reference);
    }
};


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


function _handle_HierarchicalReference(node, reference) {

    const _private = BaseNode_getPrivate(node);
    if (_private._cache._childByNameMap) {
        const addressSpace = node.addressSpace;
        const referenceType = Reference._resolveReferenceType(addressSpace, reference);

        if (referenceType) {

            const HierarchicalReferencesType = addressSpace.findReferenceType("HierarchicalReferences");

            //xx console.log ("HierarchicalReferencesType",HierarchicalReferencesType.toString());
            if (referenceType.isSupertypeOf(HierarchicalReferencesType)) {
                assert(reference.isForward);
                const targetNode = Reference._resolveReferenceNode(addressSpace, reference);
                //Xx console.log(" adding object to map");
                _private._cache._childByNameMap[targetNode.browseName.name.toString()] = targetNode;
            }
        }
    }
}

function _remove_HierarchicalReference(node, reference) {

    const _private = BaseNode_getPrivate(node);
    if (_private._cache._childByNameMap) {
        const addressSpace = node.addressSpace;
        const referenceType = Reference._resolveReferenceType(addressSpace, reference);

        if (referenceType) {
            const HierarchicalReferencesType = addressSpace.findReferenceType("HierarchicalReferences");
            if (referenceType.isSupertypeOf(HierarchicalReferencesType)) {
                assert(reference.isForward);
                const targetNode = Reference._resolveReferenceNode(addressSpace, reference);
                //Xx console.log(" adding object to map");
                delete _private._cache._childByNameMap[targetNode.browseName.name.toString()];
            }
        }
    }
}

BaseNode.prototype.__addReference = function (reference) {

    const self = this;
    const _private = BaseNode_getPrivate(self);

    assert(reference.hasOwnProperty("referenceType"));
    //xx isForward is optional : assert(reference.hasOwnProperty("isForward"));
    assert(reference.hasOwnProperty("nodeId"));

    const addressSpace = self.addressSpace;
    reference = addressSpace.normalizeReferenceTypes([reference])[0];

    assert(reference instanceof Reference);

    const h = reference.hash;
    assert(!_private._back_referenceIdx[h], "reference exists already in _back_references");
    assert(!_private._referenceIdx[h], "reference exists already in _references");

///    self._references.push(reference);
    _private._referenceIdx[h] = reference;
    _handle_HierarchicalReference(self, reference);
    return reference;
};

/**
 * @method addReference
 * @param reference
 * @param reference.referenceType {String}
 * @param [reference.isForward = true] {Boolean}
 * @param reference.nodeId {Node|NodeId|String}
 *
 * @example
 *
 *     view.addReference({ referenceType: "Organizes", nodeId: myDevice });
 *
 * or
 *
 *     myDevice1.addReference({ referenceType: "OrganizedBy", nodeId: view });
 */
BaseNode.prototype.addReference = function (reference) {

    const self = this;

    reference = self.__addReference(reference);

    const addressSpace = this.addressSpace;
    if (!_resolveReferenceType(addressSpace, reference)) {
        throw new Error("BaseNode#addReference : invalid reference  " + reference.toString());
    }
    self._clear_caches();

    _propagate_ref(self, addressSpace, reference);
    self.install_extra_properties();
    cetools._handle_add_reference_change_event(self, reference.nodeId);

};

/***
 * @method removeReference
 * @param reference
 * @return void
 */
BaseNode.prototype.removeReference = function (reference) {

    const self = this;
    const _private = BaseNode_getPrivate(self);

    assert(reference.hasOwnProperty("referenceType"));
    //xx isForward is optional : assert(reference.hasOwnProperty("isForward"));
    assert(reference.hasOwnProperty("nodeId"));

    const addressSpace = self.addressSpace;
    reference = addressSpace.normalizeReferenceTypes([reference])[0];
    const h = reference.hash;

    const relatedNode = addressSpace.findNode(reference.nodeId);

    const invReference = new Reference({
        referenceType: reference.referenceType,
        isForward: !reference.isForward,
        nodeId: self.nodeId
    });


    if (_private._referenceIdx[h]) {
        delete _private._referenceIdx[h];
        relatedNode._remove_backward_reference(invReference);

    } else if (_private._back_referenceIdx[h]) {

        relatedNode.removeReference(invReference);
    } else {
        throw new Error("Cannot find reference " + reference);
    }

    _handle_HierarchicalReference(self, reference);

    self.uninstall_extra_properties(reference);

    self._clear_caches();

};

/**
 * Undo the effect of propagate_back_references
 * @method unpropagate_back_references
 * @private
 */
BaseNode.prototype.unpropagate_back_references = function () {

    const self = this;
    const _private = BaseNode_getPrivate(self);

    const addressSpace = self.addressSpace;
    //xx assert(addressSpace instanceof AddressSpace);
    _.forEach(_private._referenceIdx, function (reference) {

        // filter out non  Hierarchical References
        const referenceType = _resolveReferenceType(addressSpace, reference);

        // istanbul ignore next
        if (!referenceType) {
            console.error(" ERROR".red, " cannot find reference ", reference.referenceType, reference.toString());
        }

        const related_node = _resolveReferenceNode(addressSpace, reference);
        if (related_node) {
            assert(reference.nodeId.toString() !== self.nodeId.toString());
            related_node._remove_backward_reference(new Reference({
                referenceType: reference.referenceType,
                isForward: !reference.isForward,
                nodeId: self.nodeId
            }));
        } // else addressSpace may be incomplete

    });
};

BaseNode.prototype._clear_caches = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    _private._cache = {};
};

BaseNode.prototype._on_child_added = function (/*obj*/) {
    const self = this;
    self._clear_caches();
};

BaseNode.prototype._on_child_removed = function (/*obj*/) {
    const self = this;
    self._clear_caches();
};

BaseNode.prototype.getWriteMask = function () {
    return 0;
};

BaseNode.prototype.getUserWriteMask = function () {
    return 0;
};
/**
 * @method readAttribute
 * @param context {SessionContext}
 * @param attributeId {AttributeId}
 * @param [indexRange {NumericRange}]
 * @param [dataEncoding {String}]
 * @return {DataValue}
 */
BaseNode.prototype.readAttribute = function (context, attributeId, indexRange, dataEncoding) {

    assert(context instanceof SessionContext);
    const options = {};
    options.statusCode = StatusCodes.Good;
    switch (attributeId) {

        case AttributeIds.NodeId:  // NodeId
            options.value = {dataType: DataType.NodeId, value: this.nodeId};
            break;

        case AttributeIds.NodeClass: // NodeClass
            assert(_.isFinite(this.nodeClass.value));
            options.value = {dataType: DataType.Int32, value: this.nodeClass.value};
            break;

        case AttributeIds.BrowseName: // QualifiedName
            assert(this.browseName instanceof QualifiedName);
            options.value = {dataType: DataType.QualifiedName, value: this.browseName};
            break;

        case AttributeIds.DisplayName: // LocalizedText
            options.value = {dataType: DataType.LocalizedText, value: this.displayName[0]};
            break;

        case AttributeIds.Description: // LocalizedText
            options.value = {dataType: DataType.LocalizedText, value: this.description};
            break;

        case AttributeIds.WriteMask:
            options.value = {dataType: DataType.UInt32, value: this.getWriteMask()};
            break;

        case AttributeIds.UserWriteMask:
            options.value = {dataType: DataType.UInt32, value: this.getUserWriteMask()};
            break;

        default:
            options.value = null;
            //xx debugLog("class Name ", this.constructor.name, (" BaseNode : '" + this.browseName + "' nodeid=" + this.nodeId.toString()).yellow, " cannot get attribute ", AttributeNameById[attributeId], "(", attributeId, ")");
            options.statusCode = StatusCodes.BadAttributeIdInvalid;
            break;
    }
    //xx options.serverTimestamp = new Date();
    return new DataValue(options);
};

/**
 * @method writeAttribute
 * @param context {SessionContext}
 * @param writeValue {Object}
 * @param writeValue.attributeId {AttributeId}
 * @param writeValue.dataValue {DataValue}
 * @param writeValue.indexRange {NumericRange}
 * @param callback {Function}
 * @param callback.err {Error|null}
 * @param callback.statusCode {StatusCode}
 * @async
 */
BaseNode.prototype.writeAttribute = function (context, writeValue, callback) {

    assert(context instanceof SessionContext);
    assert(_.isFunction(callback));

    if (writeValue.attributeId <= 0 || writeValue.attributeId > AttributeIds.UserExecutable) {
        return callback(null, StatusCodes.BadAttributeIdInvalid);
    }
    // by default Node is read-only,
    // this method needs to be overridden to change the behavior
    callback(null, StatusCodes.BadNotWritable);
};


/**
 * @method full_name
 * @return {String} the full path name of the node
 *
 */
BaseNode.prototype.full_name = function () {

    if (this.parentNodeId) {
        const parent = this.addressSpace.findNode(this.parentNodeId);

        // istanbul ignore else
        if (parent) {
            return parent.full_name() + "." + this.browseName.toString() + "";
        } else {
            return "NOT YET REGISTERED" + this.parentNodeId.toString() + "." + this.browseName.toString() + "";
        }
    }
    return this.browseName.toString();
};

BaseNode.prototype.ownReferences = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    return _.map(_private._referenceIdx);
};
BaseNode.prototype.allReferences = function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    return [].concat(_.map(_private._referenceIdx), _.map(_private._back_referenceIdx));
};


/**
 * @method browseNodeByTargetName
 *
 * @param relativePathElement                           {RelativePathElement}
 * @param relativePathElement.targetName                {QualifiedName}
 * @param relativePathElement.targetName.name           {String}
 * @param relativePathElement.targetName.namespaceIndex {UInt32}
 * @param relativePathElement.referenceTypeId           {NodeId}
 * @param relativePathElement.isInverse                 {Boolean}
 * @param relativePathElement.includeSubtypes           {Boolean}
 *
 * @return {NodeId[]}
 */
BaseNode.prototype.browseNodeByTargetName = function (relativePathElement, isLast) {

    const self = this;
    const _private = BaseNode_getPrivate(self);


    relativePathElement.targetName = relativePathElement.targetName || new QualifiedName();
    // part 4.0 v1.03 $7.26 RelativePath
    // The BrowseName of the target node.
    // The final element may have an empty targetName. In this situation all targets of the references identified by
    // the referenceTypeId are the targets of the RelativePath.
    // The targetName shall be specified for all other elements.
    // The current path cannot be followed any further if no targets with the specified BrowseName exist.
    assert(relativePathElement.targetName instanceof QualifiedName);
    assert(relativePathElement.targetName.namespaceIndex >= 0);
    assert(relativePathElement.targetName.name.length > 0);

    // The type of reference to follow from the current node.
    // The current path cannot be followed any further if the referenceTypeId is not available on the Node instance.
    // If not specified then all References are included and the parameter includeSubtypes is ignored.
    assert(relativePathElement.hasOwnProperty("referenceTypeId"));

    // Indicates whether the inverse Reference should be followed. The inverse reference is followed if this value is TRUE.
    assert(relativePathElement.hasOwnProperty("isInverse"));

    //Indicates whether subtypes of the ReferenceType should be followed. Subtypes are included if this value is TRUE.
    assert(relativePathElement.hasOwnProperty("includeSubtypes"));


    const references = self.allReferences();

    function _check_reference(reference) {

        if (relativePathElement.referenceTypeId.isEmpty()) {
            return true;
        }
        assert(relativePathElement.referenceTypeId instanceof NodeId);
        if ((relativePathElement.isInverse && reference.isForward) ||
            (!relativePathElement.isInverse && !reference.isForward)) {
            return false;
        }
        assert(reference.hasOwnProperty("isForward"));
        const referenceType = _resolveReferenceType(self.addressSpace, reference);
        const referenceTypeId = referenceType.nodeId;

        if (sameNodeId(relativePathElement.referenceTypeId, referenceTypeId)) {
            return true;
        }
        if (relativePathElement.includeSubtypes) {
            const baseType = self.addressSpace.findNode(relativePathElement.referenceTypeId);
            if (baseType && referenceType.isSupertypeOf(baseType)) {
                return true;
            }
        }
        return false;
    }

    const nodeIdsMap = {};
    let nodeIds = [];

    for (const reference of references) {

        if (!_check_reference(reference)) {
            continue;
        }

        const obj = _resolveReferenceNode(self.addressSpace, reference);

        // istanbul ignore next
        if (!obj) {
            throw new Error(" cannot find node with id ", reference.nodeId.toString());
        }

        if (_.isEqual(obj.browseName, relativePathElement.targetName)) { // compare QualifiedName
            const key = obj.nodeId.toString();
            if (!nodeIdsMap.hasOwnProperty(key)) {
                nodeIds.push(obj.nodeId);
                nodeIdsMap[key] = obj;
            }
        }
    }
    if (self.subtypeOf) {
        // browsing also InstanceDeclarations included in base type
        const baseType = self.addressSpace.findNode(self.subtypeOf);
        const n = baseType.browseNodeByTargetName(relativePathElement, isLast);
        nodeIds = [].concat(nodeIds, n);
    }
    return nodeIds;
};

const check_flag = require("node-opcua-utils").check_flag;
const rm = ResultMask;


function _makeReferenceDescription(addressSpace, reference, resultMask) {

    const isForward = reference.isForward;

    const referenceTypeId = _resolveReferenceType(addressSpace, reference).nodeId;
    assert(referenceTypeId instanceof NodeId);

    const obj = _resolveReferenceNode(addressSpace, reference);

    let data = {};

    if (!obj) {
        // cannot find reference node
        data = {
            referenceTypeId: check_flag(resultMask, rm.ReferenceType) ? referenceTypeId : null,
            isForward: isForward,
            nodeId: reference.nodeId
        };
    } else {
        assert(reference.nodeId, obj.nodeId);
        data = {
            referenceTypeId: check_flag(resultMask, rm.ReferenceType) ? referenceTypeId : null,
            isForward: check_flag(resultMask, rm.IsForward) ? isForward : false,
            nodeId: obj.nodeId,
            browseName: check_flag(resultMask, rm.BrowseName) ? coerceQualifyName(obj.browseName) : null,
            displayName: check_flag(resultMask, rm.DisplayName) ? coerceLocalizedText(obj.displayName[0]) : null,
            nodeClass: check_flag(resultMask, rm.NodeClass) ? obj.nodeClass : NodeClass.Unspecified,
            typeDefinition: check_flag(resultMask, rm.TypeDefinition) ? obj.typeDefinition : null
        };
    }
    if (data.typeDefinition === null) {
        data.typeDefinition = resolveNodeId("i=0");
    }
    const referenceDescription = new ReferenceDescription(data);
    return referenceDescription;
}

function _constructReferenceDescription(addressSpace, references, resultMask) {
    //x assert(addressSpace instanceof AddressSpace);
    assert(_.isArray(references));
    return references.map(function (reference) {
        return _makeReferenceDescription(addressSpace, reference, resultMask);
    });
}

function referenceTypeToString(addressSpace, referenceTypeId) {

    //istanbul ignore next
    if (!referenceTypeId) {
        return "<null> ";
    } else {
        const referenceType = addressSpace.findNode(referenceTypeId);
        return referenceTypeId.toString() + " " + referenceType.browseName.toString() + "/" + referenceType.inverseName.text;
    }
}

function nodeIdInfo(addressSpace, nodeId) {

    const obj = addressSpace.findNode(nodeId);
    const name = obj ? obj.browseName.toString() : " <????>";
    return nodeId.toString() + " [ " + name + " ]";

}

function dumpReferenceDescription(addressSpace, referenceDescription) {

    assert(addressSpace.constructor.name === "AddressSpace");
    //assert(addressSpace instanceof AddressSpace);
    assert(referenceDescription.referenceTypeId); // must be known;

    console.log("referenceDescription".red);
    console.log("    referenceTypeId : ", referenceTypeToString(addressSpace, referenceDescription.referenceTypeId));
    console.log("    isForward       : ", referenceDescription.isForward ? "true" : "false");
    console.log("    nodeId          : ", nodeIdInfo(addressSpace, referenceDescription.nodeId));
    console.log("    browseName      : ", referenceDescription.browseName.toString());
    console.log("    nodeClass       : ", referenceDescription.nodeClass.toString());
    console.log("    typeDefinition  : ", nodeIdInfo(addressSpace, referenceDescription.typeDefinition));

}

function dumpReferenceDescriptions(addressSpace, referenceDescriptions) {
    assert(addressSpace);
    assert(addressSpace.constructor.name === "AddressSpace");
    assert(_.isArray(referenceDescriptions));
    referenceDescriptions.forEach(function (r) {
        dumpReferenceDescription(addressSpace, r);
    });
}

exports.dumpReferenceDescription = dumpReferenceDescription;
exports.dumpReferenceDescriptions = dumpReferenceDescriptions;

function nodeid_is_nothing(nodeid) {
    return (nodeid.value === 0 && nodeid.namespace === 0);
}

/**
 * @method normalize_referenceTypeId
 * @param addressSpace {AddressSpace}
 * @param referenceTypeId {String|NodeId|null} : the referenceType either as a string or a nodeId
 * @return {NodeId}
 */
function normalize_referenceTypeId(addressSpace, referenceTypeId) {
    if (!referenceTypeId) {
        return makeNodeId(0);
    }
    if (typeof referenceTypeId === "string") {
        const ref = addressSpace.findReferenceType(referenceTypeId);
        if (ref) {
            return ref.nodeId;
        }
    }
    let nodeId;
    try {
        nodeId = addressSpace.resolveNodeId(referenceTypeId);
    }
    catch (err) {
        console.log("cannot normalize_referenceTypeId", referenceTypeId);
        throw err;
    }
    assert(nodeId);
    return nodeId;
}

function dumpBrowseDescription(node, browseDescription) {

    const addressSpace = node.addressSpace;

    console.log(" Browse Node :");

    if (browseDescription.nodeId) {
        console.log(" nodeId : ", browseDescription.nodeId.toString().cyan);
    }

    console.log(" nodeId : ", node.browseName.toString().cyan, "(", node.nodeId.toString(), ")");
    console.log("   referenceTypeId :", referenceTypeToString(addressSpace, browseDescription.referenceTypeId));

    console.log("   browseDirection :", browseDescription.browseDirection.toString().cyan);
    console.log("   includeSubType  :", browseDescription.includeSubtypes ? "true" : "false");
    console.log("   nodeClassMask   :", browseDescription.nodeClassMask);
    console.log("   resultMask      :", browseDescription.resultMask);
}

/**
 * @method dumpReferences
 * @param addressSpace    {AddressSpace}
 * @param references  {Array<Reference>|null}
 * @static
 */
function dumpReferences(addressSpace, references) {

    assert(addressSpace);

    _.forEach(references, function (reference) {

        const referenceType = Reference._resolveReferenceType(addressSpace, reference);
        if (!referenceType) {
            // unknown type ... this may happen when the address space is not fully build
            return;
        }
        const dir = reference.isForward ? "(=>)" : "(<-)";
        const objectName = nodeIdInfo(addressSpace, reference.nodeId);

        console.log(" referenceType : ", dir, referenceType ? referenceType.browseName.toString() : reference.referenceType.toString(), " ", objectName);
    });
}

exports.dumpBrowseDescription = dumpBrowseDescription;
exports.dumpReferences = dumpReferences;

const _resolveReferenceNode = Reference._resolveReferenceNode;
const _resolveReferenceType = Reference._resolveReferenceType;


function _filter_by_referenceType(self, browseDescription, references, referenceTypeId) {

    // make sure we have a valid referenceTypeId if not null
    if (!nodeid_is_nothing(referenceTypeId)) {

        assert(referenceTypeId instanceof NodeId);
        const referenceType = self.addressSpace.findNode(referenceTypeId);

        dumpIf(!referenceType, referenceTypeId);
        assert(referenceType instanceof ReferenceType);

        references = references.filter(function (reference) {

            const ref = _resolveReferenceType(self.addressSpace, reference);

            if (!ref) {
                return false;
            } // unknown type ... this may happen when the address space is not fully build
            assert(ref instanceof ReferenceType);

            const is_of_type = ref.nodeId.toString() === referenceType.nodeId.toString();
            if (is_of_type) {
                return true;
            }
            if (browseDescription.includeSubtypes) {
                return ref.isSupertypeOf(referenceType);
            } else {
                return false;
            }
        });
    }
    return references;
}

function forwardOnly(reference) {
    return reference.isForward;
}

function reverseOnly(reference) {
    return !reference.isForward;
}

function _filter_by_direction(references, browseDirection) {

    if (browseDirection === BrowseDirection.Both) {
        return references;
    }
    if (browseDirection === BrowseDirection.Forward) {
        return references.filter(forwardOnly);
    } else {
        return references.filter(reverseOnly);
    }
}


function _filter_by_nodeclass(self, references, nodeClassMask) {

    assert(_.isFinite(nodeClassMask));
    if (nodeClassMask === 0) {
        return references;
    }
    const addressSpace = self.addressSpace;
    return references.filter(function (reference) {

        const obj = _resolveReferenceNode(addressSpace, reference);

        if (!obj) {
            return false;
        }

        const nodeClassName = obj.nodeClass.key;

        const value = makeNodeClassMask(nodeClassName);
        return (value & nodeClassMask) === value;

    });
}

function _filter_by_userFilter(self, references, session) {

    const addressSpace = self.addressSpace;
    return references.filter(function (reference) {

        const obj = _resolveReferenceNode(addressSpace, reference);

        if (!obj) {
            return false;
        }

        return (obj._browseFilter(session));
    });
}

/**
 * browse the node to extract information requested in browseDescription
 * @method browseNode
 * @param browseDescription                 {BrowseDescription}
 * @param browseDescription.referenceTypeId {NodeId}
 * @param browseDescription.browseDirection {BrowseDirection}
 * @param browseDescription.nodeClassMask   {NodeClassMask}
 * @param browseDescription.resultMask      {UInt32}
 * @param session                           {ServerSession}
 * @return {ReferenceDescription[]}
 */
BaseNode.prototype.browseNode = function (browseDescription, session) {

    assert(_.isFinite(browseDescription.nodeClassMask));
    assert(_.isObject(browseDescription.browseDirection));

    const do_debug = false;

    //xx do_debug = ( this.browseName === "Server" );

    const self = this;
    const _private = BaseNode_getPrivate(self);

    const referenceTypeId = normalize_referenceTypeId(this.addressSpace, browseDescription.referenceTypeId);
    assert(referenceTypeId instanceof NodeId);

    const browseDirection = browseDescription.browseDirection || BrowseDirection.Both;

    const addressSpace = self.addressSpace;

    // get all possible references
    let references = [].concat(_.map(_private._referenceIdx), _.map(_private._back_referenceIdx));

    /* istanbul ignore next */
    if (do_debug) {
        console.log("all references :", self.nodeId.toString(), self.browseName.toString());
        dumpReferences(addressSpace, _.map(_private._referenceIdx));
    }

    // filter out references not matching referenceType
    references = _filter_by_referenceType(self, browseDescription, references, referenceTypeId);

    references = _filter_by_direction(references, browseDirection);

    references = _filter_by_nodeclass(self, references, browseDescription.nodeClassMask);

    references = _filter_by_userFilter(self, references, session);

    const referenceDescriptions = _constructReferenceDescription(addressSpace, references, browseDescription.resultMask);

    /* istanbul ignore next */
    if (do_debug) {
        dumpReferenceDescriptions(self.addressSpace, referenceDescriptions);
    }

    return referenceDescriptions;
};

/*
 * install hierarchical references as javascript properties
 * Components/Properties/Organizes
 */
function install_components_as_object_properties(parentObj) {

    if (!parentObj) {
        return;
    }

    const addressSpace = parentObj.addressSpace;
    const hierarchicalRefs = parentObj.findHierarchicalReferences();

    const children = hierarchicalRefs.map(function (r) {
        return _resolveReferenceNode(addressSpace, r);
    });


    for (const child of children) {

        if (!child) {
            continue;
        }
        // assumption: we ignore namespace here .
        const name = lowerFirstLetter(child.browseName.name.toString());


        if (reservedNames.hasOwnProperty(name)) {
            if (doDebug) {
                console.log(("Ignoring reserved keyword                                               " + name).bgWhite.red);
            }
            continue;
        }


        if (doDebug) {
            console.log("Installing property " + name, " on ", parentObj.browseName.toString());
        }

        /* istanbul ignore next */
        if (parentObj.hasOwnProperty(name)) {
            continue;
        }

        Object.defineProperty(parentObj, name, {
            enumerable: true,
            configurable: true, // set to true, so we can undefine later
            //xx writable: false,
            get: function () {
                return child;
            }
            //value: child
        });
    }
}

BaseNode.prototype.install_extra_properties = function () {

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

    if (addressSpace.isFrugal) {
        // skipping
        return;
    }

    install_components_as_object_properties(self);

    function install_extra_properties_on_parent(ref) {
        const node = Reference._resolveReferenceNode(addressSpace, ref);
        install_components_as_object_properties(node);
    }

    // make sure parent have extra properties updated
    const parentComponents = self.findReferences("HasComponent", false);
    const parentSubfolders = self.findReferences("Organizes", false);
    const parentProperties = self.findReferences("HasProperty", false);
    parentComponents.forEach(install_extra_properties_on_parent);
    parentSubfolders.forEach(install_extra_properties_on_parent);
    parentProperties.forEach(install_extra_properties_on_parent);
};


BaseNode.prototype.uninstall_extra_properties = function (reference) {
    const self = this;
    const addressSpace = self.addressSpace;

    if (addressSpace.isFrugal) {
        // skipping
        return;
    }
    const childNode = _resolveReferenceNode(addressSpace, reference);

    const name = lowerFirstLetter(childNode.browseName.name.toString());
    if (reservedNames.hasOwnProperty(name)) {
        if (doDebug) {
            console.log(("Ignoring reserved keyword                                               " + name).bgWhite.red);
        }
        return;
    }
    /* istanbul ignore next */
    if (!self.hasOwnProperty(name)) {
        return;
    }

    Object.defineProperty(self, name, {
        value: undefined
    });
};


function _clone_collection_new(newParent, collectionRef, optionalFilter, extraInfo) {

    const addressSpace = newParent.addressSpace;
    assert(!optionalFilter || (_.isFunction(optionalFilter.shouldKeep) && _.isFunction(optionalFilter.filterFor)));

    for (let reference of collectionRef) {

        const node = _resolveReferenceNode(addressSpace, reference);

        // ensure node is of the correct type,
        // it may happen that the xmlnodeset2 file was malformed

        // istanbul ignore next
        if (!_.isFunction(node.clone)) {
            console.log("Warning : cannot clone node ".red + node.browseName.toString() + " of class " + node.nodeClass.toString(), " while cloning ", newParent.browseName.toString());
            continue;
        }

        if (optionalFilter && !optionalFilter.shouldKeep(node)) {
            continue; // skip this node
        }

        assert(reference.isForward);
        assert(reference.referenceType instanceof NodeId, "" + reference.referenceType.toString());
        const options = {
            references: [
                {referenceType: reference.referenceType, isForward: false, nodeId: newParent.nodeId}
            ]
        };

        const clone = node.clone(options, optionalFilter, extraInfo);

        /// clone.propagate_back_references();

        if (extraInfo) {
            extraInfo.registerClonedObject(node, clone);
        }
    }
}

/**
 * clone properties and methods
 * @method _clone_children_references
 * @param newParent the new parent object to which references of type HasChild  will be attached
 * @param [optionalFilter {Function} = null] a filter
 * @param [extraInfo]
 * @return {Array}
 * @private
 */
BaseNode.prototype._clone_children_references = function (newParent, optionalFilter, extraInfo) {

    const self = this;
    assert(newParent instanceof BaseNode);
    // find all reference that derives from the Aggregates
    const aggregatesRef = self.findReferencesEx("Aggregates", BrowseDirection.Forward);
    _clone_collection_new(newParent, aggregatesRef, optionalFilter, extraInfo);

};

/**
 * @method _clone
 * @param Constructor {Function}
 * @param options {Object}
 * @param extraInfo
 * @param optionalFilter
 * @return {*}
 * @private
 */
BaseNode.prototype._clone = function (Constructor, options, optionalFilter, extraInfo) {

    const Namespace = require("./namespace").Namespace;

    const self = this;

    assert(_.isFunction(Constructor));
    assert(_.isObject(options));
    assert(!extraInfo || (_.isObject(extraInfo) && _.isFunction(extraInfo.registerClonedObject)));
    assert(!self.subtypeOf, "We do not do cloning of Type yet");

    options = _.extend(options, {
        addressSpace: self.addressSpace,
        browseName: self.browseName,
        displayName: self.displayName,
        description: self.description
    });
    options.references = options.references || [];

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

    if (!options.modellingRule) {
        if (self.modellingRule) {
            const modellingRuleNode = self.findReferencesAsObject("HasModellingRule")[0];
            assert(modellingRuleNode);
            options.references.push({
                referenceType: "HasModellingRule",
                isForward: true,
                nodeId: modellingRuleNode.nodeId
            });
        }
    } else {
        Namespace._process_modelling_rule(options.references, options.modellingRule);
    }

    options.nodeId = self.addressSpace.getOwnNamespace()._construct_nodeId(options);

    assert(options.nodeId instanceof NodeId);

    const cloneObj = new Constructor(options);
    self.addressSpace._register(cloneObj);

    const newFilter = optionalFilter ? optionalFilter.filterFor(cloneObj) : null;
    self._clone_children_references(cloneObj, newFilter, extraInfo);

    cloneObj.propagate_back_references();

    cloneObj.install_extra_properties();

    return cloneObj;

};

function indent(str, padding) {
    padding = padding || "          ";
    return str.split("\n").map(function (r) {
        return padding + r;
    }).join("\n");
}


BaseNode.prototype.toString = function (options) {

    const str = [];
    const self = this;

    if (options) {
        assert(_.isObject(options.cycleDetector));
    }
    options = options || {};

    function add(s) {
        str.push(s);
    }

    options.add = add;
    options.indent = indent;
    options.padding = options.padding || " ";
    options.cycleDetector = options.cycleDetector || {};

    self._toString(str, options);

    return str.join("\n");
};

const hasTypeDefinition_ReferenceTypeNodeId = resolveNodeId("HasTypeDefinition");

BaseNode.prototype._toString = function (str, options) {

    assert(_.isArray(str));

    options.level = options.level || 1;

    const add = options.add;
    const indent = options.indent;

    const self = this;
    const _private = BaseNode_getPrivate(self);


    function set_as_processed(nodeId) {
        assert(nodeId instanceof NodeId);
        options.cycleDetector[nodeId.toString()] = nodeId;
    }

    set_as_processed(self.nodeId);

    function is_already_processed(nodeId) {
        return !!options.cycleDetector[nodeId.toString()];
    }


    add("");
    add(options.padding + "          nodeId        : ".yellow + self.nodeId.toString());
    add(options.padding + "          nodeClass     : ".yellow + self.nodeClass.toString());
    add(options.padding + "          browseName    : ".yellow + self.browseName.toString());
    add(options.padding + "          displayName   : ".yellow + self.displayName.map(function (f) {
        return f.locale + " " + f.text;
    }).join(" | "));

    add(options.padding + "          description   : ".yellow + (self.description ? self.description.toString() : ""));


    if (self.dataType) {

        const addressSpace = self.addressSpace;
        const d = addressSpace.findNode(self.dataType);
        const n = d ? "(" + d.browseName.toString() + ")" : " (???)";
        add(options.padding + "                dataType: ".yellow + self.dataType + "  " + n);
    }
    if (self._dataValue) {
        add(options.padding + "                   value: ".yellow + "\n" + indent(self._dataValue.toString(), options.padding + "                        | "));
    }
    if (self.subtypeOfObj) {
        add(options.padding + "               subtypeOf: ".yellow + " " + self.subtypeOfObj.nodeId.toString() + " " + self.subtypeOfObj.browseName.toString());
    }
    if (self.typeDefinitionObj) {
        add(options.padding + "          typeDefinition: ".yellow + " " + self.typeDefinitionObj.nodeId.toString() + " " + self.typeDefinitionObj.browseName.toString());
    }

    if (self.accessLevel) {
        add(options.padding + "             accessLevel: ".yellow + " " + self.accessLevel.toString());
    }
    if (self.userAccessLevel) {
        add(options.padding + "         userAccessLevel: ".yellow + " " + self.userAccessLevel.toString());
    }
    if (self.hasOwnProperty("valueRank")) {
        add(options.padding + "               valueRank: ".yellow + " " + self.valueRank.toString());
    }
    if (self.minimumSamplingInterval !== undefined) {
        add(options.padding + " minimumSamplingInterval: ".yellow + " " + self.minimumSamplingInterval.toString() + " ms");
    }


    add(options.padding + "          references    : ".yellow + "  length =" + Object.keys(_private._referenceIdx).length);

    const dispOptions = {
        addressSpace: self.addressSpace
    };


    function dump_reference(follow, reference) {
        //xx if (!reference.isForward) {
        //xx     return;
        //xx }
        const o = _resolveReferenceNode(self.addressSpace, reference);
        const name = o ? o.browseName.toString() : "<???>";
        add(options.padding + "               +-> ".yellow + reference.toString(dispOptions) + " " + name.cyan);

        // ignore HasTypeDefinition as it has been already handled
        if (sameNodeId(reference.referenceType, hasTypeDefinition_ReferenceTypeNodeId) && reference.nodeId.namespace === 0) {
            return;
        }
        if (o) {
            if (!is_already_processed(o.nodeId)) {
                set_as_processed(o.nodeId);
                if (options.level > 1 && follow) {
                    const rr = o.toString({
                        level: options.level - 1,
                        padding: options.padding + "         ",
                        cycleDetector: options.cycleDetector
                    });
                    add(rr);
                }
            }
        }
    }

    // direct reference
    _.forEach(_private._referenceIdx, dump_reference.bind(null, true));

    const br = _.map(_private._back_referenceIdx);
    add(options.padding + "         back_references: ".yellow + "  length =" + br.length + " ( references held by other nodes involving this node)".grey);
    // backward reference
    br.forEach(dump_reference.bind(null, false));

};

/**
 * the dispose method should be called when the node is no longer used, to release
 * back pointer to the address space and clear caches.
 *
 * @method dispose
 *
 */
BaseNode.prototype.dispose = function () {

    const self = this;
    const _private = BaseNode_getPrivate(self);

    self.emit("dispose");

    self.removeAllListeners();
    self._clear_caches();

    _.forEach(_private._back_referenceIdx, function (ref) {
        ref.dispose();
    });
    _.forEach(_private._referenceIdx, function (ref) {
        ref.dispose();
    });
    _private._cache = {};
    _private.__address_space = null;
    _private._back_referenceIdx = null;
    _private._referenceIdx = null;

};


exports.BaseNode = BaseNode;
ReferenceType = require("./referenceType").ReferenceType;


/**
 * @property modellingRule
 * @type {String|undefined}
 */
BaseNode.prototype.__defineGetter__("modellingRule", function () {
    const node = this;
    let r = node.findReferencesAsObject("HasModellingRule");
    if (!r || r.length === 0) {
        return null; ///"? modellingRule missing ?"; // consider "Mandatory"
    }
    r = r[0];
    return r.browseName.toString();
});

/**
 * @property isTrueSubStateOf
 * @type {BaseNode|null}
 */
BaseNode.prototype.__defineGetter__("isTrueSubStateOf", function () {
    const node = this;
    let r = node.findReferencesAsObject("HasTrueSubState", false);
    if (!r || r.length === 0) {
        return null;
    }
    assert(r.length === 1);
    r = r[0];
    return r;
});
/**
 * @property isFalseSubStateOf
 * @type {BaseNode|null}
 */
BaseNode.prototype.__defineGetter__("isFalseSubStateOf", function () {
    const node = this;
    let r = node.findReferencesAsObject("HasFalseSubState", false);
    if (!r || r.length === 0) {
        return null;
    }
    assert(r.length === 1);
    r = r[0];
    return r;
});


/**
 * @method getFalseSubStates
 * @return {BaseNode[]} return an array with the SubStates of this object.
 */
BaseNode.prototype.getFalseSubStates = function () {
    return this.findReferencesAsObject("HasFalseSubState");
};

/**
 * @method getTrueSubStates
 * @return {BaseNode[]} return an array with the SubStates of this object.
 */
BaseNode.prototype.getTrueSubStates = function () {
    return this.findReferencesAsObject("HasTrueSubState");
};


BaseNode.prototype.findHierarchicalReferences = function () {

    const node = this;
    return node.findReferencesEx("HierarchicalReferences", BrowseDirection.Forward);
    // const _private = BaseNode_getPrivate(node);
    //
    // if (!_private._cache._HasChildReferences) {
    //     //xx console.log("node ",node.nodeId.toString());
    //     //xx _private._cache._HasChildReferences =  node.findReferencesEx("HierarchicalReferences",BrowseDirection.Forward);
    //     const r1 =
    //     const r2 = node.findReferencesEx("Organizes",BrowseDirection.Forward);
    //     _private._cache._HasChildReferences = r1.concat(r2);
    // }
    // return _private._cache._HasChildReferences;
};

BaseNode.prototype.getChildByName = function (browseName) {

    // Attention: getChild doesn't care about namespace on browseName
    //            !!!!
    if (browseName instanceof QualifiedName) {
        browseName = browseName.name.toString();
    }
    assert(typeof browseName === "string");
    const node = this;
    const _private = BaseNode_getPrivate(node);

    const addressSpace = node.addressSpace;

    if (!_private._cache._childByNameMap) {
        _private._cache._childByNameMap = {};

        const childReferenceTypes = node.findReferencesEx("HasChild");
        for (let r of childReferenceTypes) {
            const child = _resolveReferenceNode(addressSpace, r);
            _private._cache._childByNameMap[child.browseName.name.toString()] = child;
        }

    }
    const ret = _private._cache._childByNameMap[browseName] || null;
    return ret;
};


BaseNode.prototype.__defineGetter__("addressSpace", function () {
    const self = this;
    const _private = BaseNode_getPrivate(self);
    return _private.__address_space;
});

BaseNode.prototype.__defineGetter__("namespace", function () {
    const self = this;
    return self.addressSpace.getNamespace(self.nodeId.namespace);
});

BaseNode.prototype.installPostInstallFunc = function (f) {

    if (!f) {
        // nothing to do
        return;
    }
    const self = this;

    function chain(f1, f2) {
        return function () {
            const args = arguments;
            if (f1) {
                f1.apply(this, args);
            }
            if (f2) {
                f2.apply(this, args);
            }
        };
    }

    self._postInstantiateFunc = chain(self._postInstantiateFunc, f);
};