APIs

Show:
"use strict";

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


var util = require("util");
var utils = require("node-opcua-utils");

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

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

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

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

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

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


exports.BrowseDirection = BrowseDirection;

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

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

var doDebug = false;

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




function defaultBrowseFilterFunc(session){

    return  true;
}

function _get_QualifiedBrowseName(browseName) {
    return coerceQualifyName(browseName);
}
/**
 * Base class for all address_space classes
 *
 * @class BaseNode
 *
 * @param options
 * @param options.addressSpace {AddressSpace}
 * @param options.browseName {String}
 * @param [options.displayName] {String|LocalizedText}
 * @param options.references {Reference[]}
 * @param [options.description]  {String|LocalizedText}
 * @param [options.browseFilter] {Function}
 *
 * @constructor
 *
 * 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}},
 *
 *
 */
function BaseNode(options) {

    var self = this;
    assert(this.nodeClass);
    assert(options.addressSpace); // expecting an address space
    options.references = options.references || [];

    // this.__address_space = options.addressSpace;
    // make address space non enumerable
    Object.defineProperty(this, "__address_space", {configurable: true,value: options.addressSpace, enumerable: false});

    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.toString();

    this._setDisplayName(options.displayName);


    this._setDescription(options.description);



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

    // user defined filter function for browsing
    var _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)
    options.references.forEach(function(reference) {
        self.__addReference(reference);
    });

}
util.inherits(BaseNode, EventEmitter);

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

Object.defineProperty(BaseNode.prototype, "__address_space", {
    writable: true,
    hidden: true,
    enumerable: false
});

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];
    var _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) {
    var __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) {

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


BaseNode.prototype._notifyAttributeChange = function (attributeId) {
    var self = this;
    var 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
        ;
}


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

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

    if (typeof strReference !== "string") {
        assert(strReference instanceof ReferenceType);
        strReference = strReference.browseName.toString();
    }

    var self = this;

    var hash = "_refEx_"+strReference+browseDirection.toString();
    if (self._cache[hash]) {
        return self._cache[hash];
    }

    var addressSpace = this.addressSpace;

    var referenceType = addressSpace.findReferenceType(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.nodeId instanceof NodeId);

    var keys = referenceType.getSubtypeIndex();

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


    /*
    function check_ref(reference) {
        assert(reference instanceof Reference);
        //xx assert(_.isString(reference.referenceType));
        return keys[reference.referenceType] && reference.isForward === isForward
    }

    function check_and_push(ref) {
        if (check_ref(ref)) {
            references.push(ref);
        }
    }
    _.forEach(self._referenceIdx,check_and_push);
    _.forEach(self._back_referenceIdx,check_and_push);
    */
    // faster version of the above without func call
    function process(referenceIdx) {
        var i,length,ref;
        var _hashes = _.keys(referenceIdx);
        for (i=0,length =_hashes.length;i<length;i++ ) {
            ref = referenceIdx[_hashes[i]];
            if (keys[ref.referenceType] && ref.isForward === isForward){
                references.push(ref);
            }
        }
    }
    process(self._referenceIdx);
    process(self._back_referenceIdx);
    self._cache[hash] = references;
    return references;
};

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

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


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

    var self  = this;
    isForward = utils.isNullOrUndefined(isForward) ? true : !!isForward;

    assert(_.isString(strReference));
    assert(_.isBoolean(isForward));

    var hash = "_ref_"+strReference+isForward.toString();
    if (self._cache[hash]) {
        return self._cache[hash];
    }

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


    var result = [];
    _.forEach(this._referenceIdx, function (ref) {
        if (ref.isForward === isForward) {
            if(ref.referenceType === strReference) {
                result.push(ref);
            }
        }
    });

    _.forEach(this._back_referenceIdx, function (ref) {
        if (ref.isForward === isForward) {
            if(ref.referenceType === strReference) {
                result.push(ref);
            }
        }
    });

    self._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) {

    var 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];
};


var displayWarning = true;



function toString_ReferenceDescription(ref,options) {

    var addressSpace = options.addressSpace;
    //xx assert(ref instanceof ReferenceDescription);
    var r = new Reference({
        referenceType: addressSpace.findNode(ref.referenceTypeId).browseName.toString(),
        nodeId:        ref.nodeId,
        isForward:     ref.isForward
    });
    var 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));
    assert(!this._cache.parent && "_setup_parent_item has been already called");

    var addressSpace = this.addressSpace;

    if (references.length > 0) {

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

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

                if (displayWarning) {

                    var 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;
                }
            }
            this._cache.parent = Reference._resolveReferenceNode(addressSpace,references[0]);
        }
    }
}


function _asObject(nodeIds,addressSpace) {
    function toObject(reference) {
        var 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) {

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

};

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

    var nodeIds = this.findReferences(strReference, isForward);
    var 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 () {
    if (!this._cache.typeDefinition) {
        var has_type_definition_ref = this.findReference("HasTypeDefinition", true);
        this._cache.typeDefinition = has_type_definition_ref ? has_type_definition_ref.nodeId : null;
    }
    return this._cache.typeDefinition;
});


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


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

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

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

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

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

/**
 * @method getEventSourceOfs
 * @return {BaseNode[]} return a array of the objects for which this node is an EventSource
 */
BaseNode.prototype.getEventSourceOfs = function () {
    if (!this._cache._eventSources) {
        this._cache._eventSources = this.findReferencesAsObject("HasEventSource", false);
    }
    return this._cache._eventSources;
};
/**
 * retrieve a component by name
 * @method getComponentByName
 * @param browseName
 * @return {BaseNode|null}
 */
BaseNode.prototype.getComponentByName = function (browseName) {
    assert(typeof browseName === "string");
    var components = this.getComponents();
    var select = components.filter(function (c) {
        return c.browseName.toString() === browseName;
    });
    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
 * @return {BaseNode|null}
 */
BaseNode.prototype.getPropertyByName = function (browseName) {
    assert(typeof browseName === "string");
    var properties = this.getProperties();
    var select = properties.filter(function (c) {
        return c.browseName.toString() === browseName;
    });
    assert(select.length <=1, "BaseNode#getPropertyByName found duplicated reference");
    return select.length === 1 ? select[0] : null;
};


BaseNode.prototype.getFolderElementByName = function(browseName) {
    assert(typeof browseName === "string");
    var elements = this.getFolderElements();
    var select = elements.filter(function (c) {
        return c.browseName.toString() === browseName;
    });
    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 () {

    if (!this._cache._methods) {
        var components = this.getComponents();
        var UAMethod = require("./ua_method").UAMethod;
        this._cache._methods = components.filter(function (obj) {
            return obj instanceof UAMethod;
        });
    }
    return this._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) {

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


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

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

/**
 * 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() {
    if (!this._cache._subtypeOfObj) {
        var is_subtype_of_ref = this.findReference("HasSubtype", false);
        if (is_subtype_of_ref) {
            this._cache._subtypeOfObj = Reference._resolveReferenceNode(this.addressSpace,is_subtype_of_ref);
        }
    }
    return this._cache._subtypeOfObj;
});



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

    function hasBrowseName(node) {
        return node.browseName.toString() === browseName;
    }
    var 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 () {
    if (!this._cache.namespaceUri) {
        this._cache.namespaceUri = this.addressSpace.getNamespaceUri(this.namespaceIndex);
    }
    return this._cache.namespaceUri;
});

/**
 * the parent node
 * @property parent
 * @type {BaseNode}
 */
BaseNode.prototype.__defineGetter__("parent", function () {
    if (this._cache.parent === undefined) {
        _setup_parent_item.call(this, this._referenceIdx);
    }
    return this._cache.parent;
});

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

BaseNode.prototype._remove_backward_reference = function (reference) {
    var self = this;
    assert(reference instanceof Reference);

    _remove_HierarchicalReference(self,reference);
    var h = reference.hash;
    if (self._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
        self._back_referenceIdx[h].dispose();
        delete self._back_referenceIdx[h];
    }
    reference.dispose();
};

BaseNode.prototype._add_backward_reference = function (reference) {

    var self = this;
    assert(reference instanceof Reference);
    //xx assert(Reference.is_valid_reference(reference));

    var h = reference.hash; assert(_.isString(h));
    // istanbul ignore next
    if (self._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 (self._back_referenceIdx[h]) {
        var opts = { addressSpace: self.addressSpace};
        console.warn(" Warning !",self.browseName.toString());
        console.warn("    ",reference.toString(opts));
        console.warn(" already found in ===>");
        console.warn(_.map(self._back_referenceIdx.map(function(c){ return c.toString(opts);})).join("\n"));
        console.warn("===>");
        throw new Error("reference exists already in _back_references");
    }
    self._back_referenceIdx[h] = reference;
    _handle_HierarchicalReference(self,reference);
    self._clear_caches();

};

var displayWarningReferencePointingToItsef = true;

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

}
function _propagate_ref(self, addressSpace, reference) {

    // filter out non  Hierarchical References
    var 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; }
    var 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,
            isForward: !reference.isForward,
            nodeId: self.nodeId
        }));

    } // 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 () {

    var self = this;
    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; 
    }
    var addressSpace = self.addressSpace;
    _.forEach(self._referenceIdx,function (reference) {
        _propagate_ref(self, addressSpace, reference);
    });
};


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


function _handle_HierarchicalReference(node,reference) {

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

        if (referenceType) {

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

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

    }
}
function _remove_HierarchicalReference(node,reference) {

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

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

    var self = this;

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

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

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

///    self._references.push(reference);
    self._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) {

    var self = this;

    reference = self.__addReference(reference);

    var addressSpace = this.addressSpace;
    if (!_resolveReferenceType(addressSpace,reference)) {
       throw new Error("BaseNode#addReference : invalid reference " + h + " " +reference.toString());
    }
    self._clear_caches();
    
    _propagate_ref(self, addressSpace, reference);
    self.install_extra_properties();
    cetools._handle_add_reference_change_event(self,reference.nodeId);

};

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

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

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

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

        var 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() {
    var self = this;
    // clean caches
    Object.defineProperty(this, "_cache", { configurable: true,value:{}, hidden:true,enumerable: false});
};

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

BaseNode.prototype._on_child_removed = function(/*obj*/) {
    var 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);
    var 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) {
        var 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();
};


/**
 * @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) {

    var self = this;

    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"));


    var references = [].concat(_.map(self._referenceIdx),_.map(self._back_referenceIdx));

    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"));
        var referenceType = _resolveReferenceType(self.addressSpace,reference);
        var referenceTypeId = referenceType.nodeId;

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

    var nodeIdsMap = {};
    var nodeIds = [];
    references.forEach(function (reference) {

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

        var 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
            var 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
        var baseType = self.addressSpace.findNode(self.subtypeOf);
        var n = baseType.browseNodeByTargetName(relativePathElement,isLast);
        nodeIds = [].concat(nodeIds, n);
    }
    return nodeIds;
};

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


function _makeReferenceDescription(addressSpace, reference, resultMask) {

    var isForward = reference.isForward;

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

    var obj = _resolveReferenceNode(addressSpace,reference);

    var 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");
    }
    var 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 {
        var referenceType = addressSpace.findNode(referenceTypeId);
        return referenceTypeId.toString() + " " + referenceType.browseName.toString() + "/" + referenceType.inverseName.text;
    }
}

function nodeIdInfo(addressSpace, nodeId) {

    var obj = addressSpace.findNode(nodeId);
    var 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") {
        var ref = addressSpace.findReferenceType(referenceTypeId);
        if (ref) {
            return ref.nodeId;
        }
    }
    var nodeId;
    try {
        nodeId = addressSpace.resolveNodeId(referenceTypeId);
    }
    catch (err) {
        console.log("cannot normalize_referenceTypeId", referenceTypeId);
        throw err;
    }
    assert(nodeId);
    return nodeId;
}

function dumpBrowseDescription(node, browseDescription) {

    var 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) {

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

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

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

var _resolveReferenceNode = Reference._resolveReferenceNode;
var _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);
        var referenceType = self.addressSpace.findNode(referenceTypeId);

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

        references = references.filter(function (reference) {

            // xxx if (reference.referenceType === "HasSubtype"){ return false;  }
            /// var ref = self.addressSpace.findReferenceType(reference.referenceType);
            var 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);

            var 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;
    }
    var addressSpace = self.addressSpace;
    return references.filter(function (reference) {

        var obj = _resolveReferenceNode(addressSpace,reference);

        if (!obj) {
            return false;
        }

        var nodeClassName = obj.nodeClass.key;

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

    });
}

function _filter_by_userFilter(self, references, session) {

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

        var 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));

    var do_debug = false;

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

    var self = this;

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

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

    var addressSpace = self.addressSpace;

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

    /* istanbul ignore next */
    if (do_debug) {
        console.log("all references :", self.nodeId.toString(), self.browseName.toString());
        dumpReferences(addressSpace, _.map(self._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);

    var 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;
    }

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

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


    children.forEach(function (child) {

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


        if (reservedNames.hasOwnProperty(name)) {
           if (doDebug) {console.log(("Ignoring reserved keyword                                               "+ name).bgWhite.red);}
            return;
        }
        /* istanbul ignore next */
        if (parentObj.hasOwnProperty(name)) {
            return;
        }

        Object.defineProperty(parentObj, name, {
            enumerable: true,
            configurable: false,
            //xx writable: false,
            get: function () {
                return child;
            }
            //value: child
        });
    });
}

BaseNode.prototype.install_extra_properties = function () {

    var self = this;
    var addressSpace = self.addressSpace;

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

    install_components_as_object_properties(self);

    function install_extra_properties_on_parent(ref) {
        var node = Reference._resolveReferenceNode(addressSpace,ref);
        install_components_as_object_properties(node);
    }
    // make sure parent have extra properties updated
    var components = self.findReferences("HasComponent", false);
    var subfolders = self.findReferences("Organizes", false);
    var properties = self.findReferences("HasProperty", false);
    components.forEach(install_extra_properties_on_parent);
    subfolders.forEach(install_extra_properties_on_parent);
    properties.forEach(install_extra_properties_on_parent);


};

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

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

    collectionRef.forEach(function (reference) {

        var 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());
            return;
        }

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

        var options = {
            references: [
                {referenceType: reference.referenceType, isForward: false, nodeId: newParent.nodeId}
            ]
        };

        var 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) {

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

};

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

    var 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 (self.modellingRule) {
        var modellingRuleNode = self.findReferencesAsObject("HasModellingRule")[0];
        assert(modellingRuleNode);
        {
            options.references.push({referenceType: "HasModellingRule", isForward: true, nodeId: modellingRuleNode.nodeId});
        }
    }


    options.nodeId = self.addressSpace._construct_nodeId(options);
    assert(options.nodeId instanceof NodeId);

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

    var newFilter = optionalfilter? optionalfilter.filterFor(cloneObj) :null;
    self._clone_children_references(cloneObj,newFilter,extraInfo);

    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) {

    var str = [];
    var 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");
};

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

    assert(_.isArray(str));

    options.level = options.level || 1;

    var add = options.add;
    var indent = options.indent;

    var self = this;

    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) {

        var addressSpace = self.addressSpace;
        var d = addressSpace.findNode(self.dataType);
        var 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(self._referenceIdx).length);

    var dispOptions = {
        addressSpace: self.addressSpace
    };


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

        // ignore HasTypeDefinition as it has been already handled
        if (reference.referenceType === "HasTypeDefinition" && reference.nodeId.namespace === 0) {
            return;
        }
        if (o) {
            if (!is_already_processed(o.nodeId)) {
                set_as_processed(o.nodeId);
                if (options.level > 1 && follow) {
                    var rr = o.toString({
                        level: options.level-1,
                        padding: options.padding + "         ",
                        cycleDetector: options.cycleDetector
                    });
                    add(rr);
                }
            }
        }
    }
    // direct reference
    _.forEach(self._referenceIdx,dump_reference.bind(null,true));

    var  br = _.map(self._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() {

    var self = this;
    self.emit("dispose");

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

    _.forEach(self._back_referenceIdx,function(ref){ ref.dispose(); });
    _.forEach(self._referenceIdx,function(ref){ ref.dispose(); });

    Object.defineProperty(self, "__address_space",       {value: null, hidden: true, enumerable: false});
    Object.defineProperty(self, "_cache",                {value: null, hidden: true, enumerable: false});
    Object.defineProperty(self, "_back_referenceIdx",    {value: null, hidden: true, enumerable: false});
    Object.defineProperty(self, "_referenceIdx",         {value: null, hidden: true, enumerable: false});


};


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


/**
 * @property modellingRule
 * @type {String|undefined}
 */
BaseNode.prototype.__defineGetter__("modellingRule",function() {
    var node = this;
    var 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() {
    var node = this;
    var 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() {
    var node = this;
    var 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() {

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

BaseNode.prototype.getChildByName = function(browseName) {

    var node = this;

    browseName = browseName.toString();

    var addressSpace = node.addressSpace;

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

        var childrenRef = node.findHierarchicalReferences();
        _.forEach(childrenRef,function(r){
            var child = _resolveReferenceNode(addressSpace,r);
            node._cache._childByNameMap[child.browseName.toString()] = child;
        });
    }
    var ret = node._cache._childByNameMap[browseName] || null;
    return ret;
};


BaseNode.prototype.__defineGetter__("addressSpace",function(){ return this.__address_space;});

BaseNode.prototype.installPostInstallFunc = function(f) {

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

    function chain(f1,f2) {
        return function() {
            var args = arguments;
            if(f1) {
                f1.apply(this,args);
            }
            if (f2) {
                f2.apply(this,args);
            }
        }
    }
    self._postInstantiateFunc = chain(self._postInstantiateFunc,f);
};