APIs

Show:
"use strict";
const async = require("async");
const _ = require("underscore");
const util = require("util");
const EventEmitter = require("events").EventEmitter;
const assert = require("node-opcua-assert").assert;


const AttributeIds        = require("node-opcua-data-model").AttributeIds;
const BrowseDirection     = require("node-opcua-data-model").BrowseDirection;
const QualifiedName       = require("node-opcua-data-model").QualifiedName;
const LocalizedText       = require("node-opcua-data-model").LocalizedText;
const NodeClass           = require("node-opcua-data-model").NodeClass;

const BrowseDescription   = require("node-opcua-service-browse").BrowseDescription;

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

const browse_service     = require("node-opcua-service-browse");

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

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

const debugLog = require("node-opcua-debug").make_debugLog(__filename);

//
// some server do not expose the ReferenceType Node in their address space
// ReferenceType are defined by the OPCUA standard and can be prepopulated in the crawler.
// Pre-populating the ReferenceType node in the crawler will also reduce the network traffic.
//
const ReferenceTypeIds = require("node-opcua-constants").ReferenceTypeIds;

/*=
 *
 * @param arr
 * @param maxNode
 * @private
 * @return {*}
 */
function _fetch_elements(arr, maxNode) {
    assert(_.isArray(arr));
    assert(arr.length > 0);
    const high_limit = ( maxNode <= 0) ? arr.length : maxNode;
    const tmp = arr.splice(0, high_limit);
    assert(tmp.length > 0);
    return tmp;
}

/**
 * @class NodeCrawler
 * @param session
 * @constructor
 */
function NodeCrawler(session) {

    const self = this;

    self.session = session;
    // verify that session object provides the expected methods (browse/read)
    assert(_.isFunction(session.browse));
    assert(_.isFunction(session.read));

    self.browseNameMap = {};
    self._nodesToReadEx = [];
    self._objectCache = {};
    self._objectToBrowse = [];
    self._objMap = {};

    this._initialize_referenceTypeId();

    self.q = async.queue(function (task, callback) {

        // use process next tick to relax the stack frame
        setImmediate(function () {
            task.func.call(self, task, function () {
                self.resolve_deferred_browseNode();
                self.resolve_deferred_readNode();
                callback();
            });
        });

    }, 1);

    // MaxNodesPerRead from Server.ServerCapabilities.OperationLimits
    // VariableIds.ServerType_ServerCapabilities_OperationLimits_MaxNodesPerRead
    self.maxNodesPerRead = 0;

    //  MaxNodesPerBrowse from Server.ServerCapabilities.OperationLimits
    //  VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerBrowse
    self.maxNodesPerBrowse = 0; // 0 = no limits

    // statistics
    self.startTime = new Date();
    self.readCounter = 0;
    self.browseCounter = 0;
    self.transactionCounter = 0;
}

util.inherits(NodeCrawler, EventEmitter);

NodeCrawler.prototype._initialize_referenceTypeId = function () {

    const self = this;

    function append_prepopulated_reference(browseName) {

        const nodeId = makeNodeId(ReferenceTypeIds[browseName], 0);
        assert(nodeId);
        const cacheNode = self._createCacheNode(nodeId);
        cacheNode.browseName = new QualifiedName({name: browseName});
    }

    //  References
    //  +->(hasSubtype) NoHierarchicalReferences
    //                  +->(hasSubtype) HasTypeDefinition
    //  +->(hasSubtype) HierarchicalReferences
    //                  +->(hasSubtype) HasChild/ChildOf
    //                                  +->(hasSubtype) Aggregates/AggregatedBy
    //                                                  +-> HasProperty/PropertyOf
    //                                                  +-> HasComponent/ComponentOf
    //                                                  +-> HasHistoricalConfiguration/HistoricalConfigurationOf
    //                                 +->(hasSubtype) HasSubtype/HasSupertype
    //                  +->(hasSubtype) Organizes/OrganizedBy
    //                  +->(hasSubtype) HasEventSource/EventSourceOf
    append_prepopulated_reference("HasTypeDefinition");
    append_prepopulated_reference("HasChild");
    append_prepopulated_reference("HasProperty");
    append_prepopulated_reference("HasComponent");
    append_prepopulated_reference("HasHistoricalConfiguration");
    append_prepopulated_reference("HasSubtype");
    append_prepopulated_reference("Organizes");
    append_prepopulated_reference("HasEventSource");

};

NodeCrawler.prototype._readOperationalLimits = function (callback) {

    const self = this;
    const n1 = makeNodeId(VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead);
    const n2 = makeNodeId(VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerBrowse);
    const nodesToRead = [
        {nodeId: n1, attributeId: AttributeIds.Value},
        {nodeId: n2, attributeId: AttributeIds.Value}
    ];
    self.transactionCounter++;
    self.session.read(nodesToRead, function (err, dataValues) {
        if (!err) {
            if (dataValues[0].statusCode.equals(StatusCodes.Good)) {
                self.maxNodesPerRead = dataValues[0].value.value;
            }
            // ensure we have a sensible maxNodesPerRead value in case the server doesn't specify one
            self.maxNodesPerRead = self.maxNodesPerRead || 500;

            if (dataValues[1].statusCode.equals(StatusCodes.Good)) {
                self.maxNodesPerBrowse = dataValues[1].value.value;
            }
            // ensure we have a sensible maxNodesPerBrowse value in case the server doesn't specify one
            self.maxNodesPerBrowse = self.maxNodesPerBrowse || 500;

        }
        callback(err);
    });
};

function make_node_attribute_key(nodeId, attributeId) {
    const key = nodeId.toString() + "_" + attributeId.toString();
    return key;
}

NodeCrawler.prototype.set_cache_NodeAttribute = function (nodeId, attributeId, value) {
    const self = this;
    const key = make_node_attribute_key(nodeId, attributeId);
    self.browseNameMap[key] = value;
};

NodeCrawler.prototype.has_cache_NodeAttribute = function (nodeId, attributeId) {
    const self = this;
    const key = make_node_attribute_key(nodeId, attributeId);
    return self.browseNameMap.hasOwnProperty(key);
};

NodeCrawler.prototype.get_cache_NodeAttribute = function (nodeId, attributeId) {
    const self = this;
    const key = make_node_attribute_key(nodeId, attributeId);
    return self.browseNameMap[key];
};

/**
 * request a read operation for a Node+Attribute in the future, provides a callback
 *
 * @method _defer_readNode
 * @param nodeId {NodeId}
 * @param attributeId {AttributeId}
 * @param {function} callback
 * @param {Error|null} callback.err
 * @param {string} callback.dataValue
 * @private
 */



NodeCrawler.prototype._defer_readNode = function (nodeId, attributeId, callback) {
    const self = this;

    nodeId = resolveNodeId(nodeId);
    assert(nodeId instanceof NodeId);

    const key = make_node_attribute_key(nodeId, attributeId);

    if (self.has_cache_NodeAttribute(nodeId, attributeId)) {
        callback(null, self.get_cache_NodeAttribute(nodeId, attributeId));
    } else {
        self.browseNameMap[key] = "?";
        self._nodesToReadEx.push({
            nodeToRead: {
                nodeId: nodeId,
                attributeId: attributeId
            },
            action: function (dataValue) {
                self.set_cache_NodeAttribute(nodeId, attributeId, dataValue);
                callback(null, dataValue);
            }
        });
    }
};

/**
 * perform pending read Node operation
 * @method _resolve_deferred_readNode
 * @param callback {Function}
 * @private
 */
NodeCrawler.prototype._resolve_deferred_readNode = function (callback) {

    const self = this;
    if (self._nodesToReadEx.length === 0) {
        // nothing to read
        callback();
        return;
    }

    const _nodesToReadEx = _fetch_elements(self._nodesToReadEx, self.maxNodesPerRead);

    const nodesToRead = _nodesToReadEx.map(function (e) {
        return e.nodeToRead;
    });

    self.readCounter += nodesToRead.length;
    self.transactionCounter++;

    self.session.read(nodesToRead, function (err, dataValues /*, diagnostics*/) {

        if (!err) {

            for (const pair of _.zip(_nodesToReadEx, dataValues)) {
                const _nodeToReadEx = pair[0];

                const dataValue = pair[1];
                assert(dataValue.hasOwnProperty("statusCode"));
                if (dataValue.statusCode.equals(StatusCodes.Good)) {
                    if (dataValue.value === null) {
                        _nodeToReadEx.action(null);
                    }
                    else {
                        _nodeToReadEx.action(dataValue.value.value);
                    }
                } else {
                    _nodeToReadEx.action({name: dataValue.statusCode.key});
                }

            }
        }
        callback(err);
    });
};

NodeCrawler.prototype._resolve_deferred = function (comment, collection, method) {
    const self = this;

    if (collection.length > 0) {
        self._push_task("adding operation " + comment, {
            params: {},
            func: function (task, callback) {
                method.call(self, callback);
            }
        });
    }
};

NodeCrawler.prototype.resolve_deferred_readNode = function () {
    this._resolve_deferred("read_node", this._nodesToReadEx, this._resolve_deferred_readNode);
};

NodeCrawler.prototype.resolve_deferred_browseNode = function () {
    this._resolve_deferred("browse_node", this._objectToBrowse, this._resolve_deferred_browseNode);
};

const pendingBrowseName = new QualifiedName({name: "pending"});
function CacheNode(nodeId) {
    /**
     * @property nodeId
     * @type NodeId
     */
    this.nodeId = nodeId;
    /**
     * @property browseName
     * @type     QualifiedName
     */
    this.browseName = pendingBrowseName;
    /**
     * @property references
     * @type ReferenceDescription[]
     */
    this.references = [];
}

function w(s, l) {
    return (s + "                                                                ").substr(0, l);
}
CacheNode.prototype.toString = function () {
    let str = w(this.nodeId.toString(), 20);
    str += " " + w(this.browseName.toString(), 30);
    str += " typeDef : " + w((this.typeDefinition ? this.typeDefinition.toString() : ""), 30);
    str += " nodeClass : " + w((this.nodeClass ? this.nodeClass.toString() : ""), 12);
    return str;
};

NodeCrawler.prototype._getCacheNode = function (nodeId) {
    const self = this;
    const key = resolveNodeId(nodeId).toString();
    const cacheNode = self._objectCache[key];
    return cacheNode;
};

NodeCrawler.prototype._createCacheNode = function (nodeId) {
    const self = this;
    const key = resolveNodeId(nodeId).toString();
    let cacheNode = self._objectCache[key];
    if (cacheNode) {
        throw new Error("NodeCrawler#_createCacheNode : cache node should not exist already : " + nodeId.toString());
    }
    cacheNode = new CacheNode(nodeId);
    assert(!self._objectCache.hasOwnProperty(key));
    self._objectCache[key] = cacheNode;
    return cacheNode;
};

/**
 * perform a deferred browse
 * instead of calling session.browse directly, this function add the request to a list
 * so that request can be grouped and send in one single browse command to the server.
 *
 * @method _defer_browse_node
 * @private
 * @param cacheNode                {CacheNode}
 * @param referenceTypeId       {string|ReferenceType}
 * @param actionOnBrowse        {Function}
 * @param actionOnBrowse.err    {Error|null}
 * @param actionOnBrowse.object {CacheNode}
 *
 */
NodeCrawler.prototype._defer_browse_node = function (cacheNode, referenceTypeId, actionOnBrowse) {
    const self = this;
    self._objectToBrowse.push({
        cacheNode: cacheNode,
        nodeId: cacheNode.nodeId,
        referenceTypeId: referenceTypeId,
        action: function (object) {
            assert(object === cacheNode);
            assert(_.isArray(object.references));
            assert(cacheNode.browseName.name !== "pending");
            actionOnBrowse(null, cacheNode);
        }
    });
};

const referencesId = resolveNodeId("References");
const hierarchicalReferencesId = resolveNodeId("HierarchicalReferences");
const hasTypeDefinitionId = resolveNodeId("HasTypeDefinition");


function dedup_reference(references) {
    const results = [];
    const dedup = {};
    for (const reference of  references) {
        const key = reference.referenceTypeId.toString() + reference.nodeId.toString();
        if (dedup[key]) {
            console.log(" Warning => Duplicated reference found  !!!! please contact the server vendor");
            console.log(reference.toString());
            continue;
        }
        dedup[key] = reference;
        results.push(reference);
    }
    return results;
}
/**
 * @method _process_single_browseResult
 * @param _objectToBrowse
 * @param browseResult {BrowseResult}
 * @private
 */
NodeCrawler.prototype._process_single_browseResult = function (_objectToBrowse, browseResult) {

    const self = this;

    assert(browseResult.continuationPoint === null, "NodeCrawler doesn't support continuation point yet");

    const cacheNode = _objectToBrowse.cacheNode;

    // note : some OPCUA may expose duplicated reference, they need to be filtered out
    // dedup reference

    cacheNode.references = dedup_reference(browseResult.references);

    const tmp = browseResult.references.filter(function (x) {
        return sameNodeId(x.referenceTypeId, hasTypeDefinitionId);
    });
    if (tmp.length) {
        cacheNode.typeDefinition = tmp[0].nodeId;
    }

    async.parallel([

          function (callback) {
              if (cacheNode.browseName !== pendingBrowseName) {
                  return callback();
              }
              self._defer_readNode(cacheNode.nodeId, AttributeIds.BrowseName, function (err, browseName) {
                  cacheNode.browseName = browseName;
                  callback();
              });
          },
          function (callback) {
              if (cacheNode.displayName) {
                  return callback();
              }
              self._defer_readNode(cacheNode.nodeId, AttributeIds.DisplayName, function (err, value) {
                  if (err) {
                      return callback(err);
                  }
                  if (!(value instanceof LocalizedText)) {
                      console.log(cacheNode.toString())
                  }
                  assert(value instanceof LocalizedText);
                  cacheNode.displayName = value;
                  setImmediate(callback);
              });
          },
          function (callback) {
              // only if nodeClass is Variable
              if (cacheNode.nodeClass !== NodeClass.Variable) {
                  return callback();
              }
              // read dataType and DataType if node is a variable
              self._defer_readNode(cacheNode.nodeId, AttributeIds.DataType, function (err, dataType) {

                  if (!(dataType instanceof NodeId)) {
                      return callback();
                  }
                  cacheNode.dataType = dataType;
                  callback();
              });
          },
          function (callback) {
              if (cacheNode.nodeClass !== NodeClass.Variable) {
                  return callback();
              }
              self._defer_readNode(cacheNode.nodeId, AttributeIds.Value, function (err, value) {
                  cacheNode.dataValue = value;
                  callback();
              });
          },
          function (callback) {
              if (cacheNode.nodeClass !== NodeClass.Variable) {
                  return callback();
              }
              self._defer_readNode(cacheNode.nodeId, AttributeIds.MinimumSamplingInterval, function (err, value) {
                  cacheNode.minimumSamplingInterval = value;
                  callback();
              });
          },
          function (callback) {
              if (cacheNode.nodeClass !== NodeClass.Variable) {
                  return callback();
              }
              self._defer_readNode(cacheNode.nodeId, AttributeIds.AccessLevel, function (err, value) {
                  cacheNode.accessLevel = value;
                  callback();
              });
          },
          function (callback) {
              if (cacheNode.nodeClass !== NodeClass.Variable) {
                  return callback();
              }
              self._defer_readNode(cacheNode.nodeId, AttributeIds.UserAccessLevel, function (err, value) {
                  cacheNode.userAccessLevel = value;
                  callback();
              });
          }
      ], function (err) {
          if (err) {
              console.log("ERRROR".red, err);
          }
          _objectToBrowse.action(cacheNode);
      }
    );
};

NodeCrawler.prototype._process_browse_response = function (task, callback) {
    /* jshint validthis: true */
    const self = this;
    const objectsToBrowse = task.param.objectsToBrowse;
    const browseResults = task.param.browseResults;
    for (const pair of _.zip(objectsToBrowse, browseResults)) {
        const objectToBrowse = pair[0];
        const browseResult = pair[1];
        assert(browseResult instanceof browse_service.BrowseResult);
        self._process_single_browseResult(objectToBrowse, browseResult);
    }
    setImmediate(callback);
};

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

//                         "ReferenceType | IsForward | BrowseName | NodeClass | DisplayName | TypeDefinition"
const resultMask = makeResultMask("ReferenceType | IsForward | BrowseName | DisplayName | NodeClass | TypeDefinition");

NodeCrawler.prototype._resolve_deferred_browseNode = function (callback) {

    const self = this;

    if (self._objectToBrowse.length === 0) {
        callback();
        return;
    }

    const objectsToBrowse = _fetch_elements(self._objectToBrowse, self.maxNodesPerBrowse);

    const nodesToBrowse = objectsToBrowse.map(function (e) {
        assert(e.hasOwnProperty("referenceTypeId"));
        return new BrowseDescription({
            browseDirection: BrowseDirection.Forward,
            referenceTypeId: e.referenceTypeId,
            includeSubtypes: true,
            nodeId: e.nodeId,
            resultMask: resultMask
        });
    });

    self.browseCounter += nodesToBrowse.length;
    self.transactionCounter++;

    self.session.browse(nodesToBrowse, function (err, browseResults /*, diagnostics*/) {

        if (!err) {
            assert(browseResults.length === nodesToBrowse.length);
            self._unshift_task("process browseResults", {
                param: {
                    objectsToBrowse: objectsToBrowse,
                    browseResults: browseResults
                },
                func: NodeCrawler.prototype._process_browse_response
            });

        } else {
            console.log("ERROR = ", err);
        }
        callback(err);

    });
};

/**
 * @method _unshift_task
 * add a task on top of the queue (high priority)
 * @param name {string}
 * @param task
 * @private
 */
NodeCrawler.prototype._unshift_task = function (name, task) {
    const self = this;
    assert(_.isFunction(task.func));
    task.comment = "S:" + name;
    assert(task.func.length === 2);
    self.q.unshift(task);
};

/**
 * @method _push_task
 * add a task at the bottom of the queue (low priority)
 * @param name {string}
 * @param task
 * @private
 */
NodeCrawler.prototype._push_task = function (name, task) {
    const self = this;
    assert(_.isFunction(task.func));
    task.comment = "P:" + name;
    assert(task.func.length === 2);
    self.q.push(task);
};


NodeCrawler.follow = function (crawler, cacheNode, userData) {
    for (const reference of cacheNode.references) {
        crawler.followReference(cacheNode, reference, userData);
    }
};
/***
 * @method _emit_on_crawled
 * @param cacheNode
 * @param userData
 * @param [userData.onBrowsed=null] {Function}
 * @private
 */
NodeCrawler.prototype._emit_on_crawled = function (cacheNode, userData) {
    const self = this;
    self.emit("browsed", cacheNode, userData);
};


NodeCrawler.prototype._crawl_task = function (task, callback) {

    const self = this;

    const cacheNode = task.params.cacheNode;
    let nodeId = task.params.cacheNode.nodeId;

    nodeId = resolveNodeId(nodeId);
    const key = nodeId.toString();

    if (self._visited_node.hasOwnProperty(key)) {
        console.log("skipping already visited", key);
        callback();
        return;// already visited
    }
    // mark as visited to avoid infinite recursion
    self._visited_node[key] = true;

    function browse_node_action(err, cacheNode) {
        if (!err) {
            for (let i = 0; i < cacheNode.references.length; i++) {

                const reference = cacheNode.references[i];
                // those ones come for free
                if (!self.has_cache_NodeAttribute(reference.nodeId, AttributeIds.BrowseName)) {
                    self.set_cache_NodeAttribute(reference.nodeId, AttributeIds.BrowseName, reference.browseName);
                }
                if (!self.has_cache_NodeAttribute(reference.nodeId, AttributeIds.DisplayName)) {
                    self.set_cache_NodeAttribute(reference.nodeId, AttributeIds.DisplayName, reference.displayName);
                }
                if (!self.has_cache_NodeAttribute(reference.nodeId, AttributeIds.NodeClass)) {
                    self.set_cache_NodeAttribute(reference.nodeId, AttributeIds.NodeClass, reference.nodeClass);
                }
            }
            self._emit_on_crawled(cacheNode, task.params.userData);
            const userData = task.params.userData;
            if (userData.onBrowse) {
                userData.onBrowse(self, cacheNode, userData);
            }

        }
    }

    self._defer_browse_node(cacheNode, referencesId, browse_node_action);
    callback();
};

NodeCrawler.prototype._add_crawl_task = function (cacheNode, userData) {
    /* jshint validthis: true */
    const self = this;
    assert(cacheNode instanceof CacheNode);
    assert(userData);
    assert(_.isObject(self));
    assert(_.isObject(self._crawled));

    const key = cacheNode.nodeId.toString();
    if (self._crawled.hasOwnProperty(key)) {
        return;
    }
    self._crawled[key] = 1;

    self._push_task("_crawl task", {
        params: {
            cacheNode: cacheNode,
            userData: userData
        },
        func: NodeCrawler.prototype._crawl_task
    });
};

NodeCrawler.prototype._inner_crawl = function (nodeId, userData, end_callback) {

    const self = this;
    assert(_.isObject(userData));
    assert(_.isFunction(end_callback));
    assert(!self._visited_node);
    assert(!self._crawled);
    self._visited_node = {};
    self._crawled = {};

    let _has_ended = false;
    self.q.drain = function () {

        if (!_has_ended) {

            _has_ended = true;

            self._visited_node = null;
            self._crawled = null;

            self.emit("end");
            end_callback();
        }
    };


    let cacheNode = self._getCacheNode(nodeId);
    if (!cacheNode) {
        cacheNode = self._createCacheNode(nodeId);
    }

    assert(cacheNode.nodeId.toString() === nodeId.toString());

    // ----------------------- Read missing essential information about node
    // such as nodeClass, typeDefinition browseName, displayName
    // this sequence is only necessary on the top node being crawled,
    // as browseName,displayName,nodeClass will be provided by ReferenceDescription later on for child nodes
    //
    async.parallel([
        function (callback) {
            self._defer_readNode(cacheNode.nodeId, AttributeIds.BrowseName, function (err, value) {
                if (err) {
                    return callback(err);
                }
                assert(value instanceof QualifiedName);
                cacheNode.browseName = value;
                setImmediate(callback);
            });
        },
        function (callback) {
            self._defer_readNode(cacheNode.nodeId, AttributeIds.NodeClass, function (err, value) {
                if (err) {
                    return callback(err);
                }
                cacheNode.nodeClass = NodeClass.get(value);
                setImmediate(callback);
            });
        },
        function (callback) {
            self._defer_readNode(cacheNode.nodeId, AttributeIds.DisplayName, function (err, value) {
                if (err) {
                    return callback(err);
                }
                assert(value instanceof LocalizedText);
                cacheNode.displayName = value;
                setImmediate(callback);
            });
        },
        function (callback) {
            self._resolve_deferred_readNode(callback);
        }
    ], function (err) {
        self._add_crawl_task(cacheNode, userData);
    });


};

/**
 * @method crawl
 * @param nodeId {NodeId}
 * @param userData {Object}
 * @param userData.onBrowse           {Function}
 * @param userData.onBrowse.crawler   {NodeCrawler}
 * @param userData.onBrowse.cacheNode {CacheNode}
 * @param end_callback {Function}
 */
NodeCrawler.prototype.crawl = function (nodeId, userData, end_callback) {
    assert(_.isFunction(end_callback));
    const self = this;
    self._readOperationalLimits(function (err) {

        if (err) {
            return end_callback(err);
        }
        self._inner_crawl(nodeId, userData, end_callback);
    });
};


function _setExtraReference(task, callback) {
    const params = task.params;
    params.userData.setExtraReference(params.parentNode, params.reference, params.childCacheNode, params.userData);
    callback();
}

NodeCrawler.prototype.followReference = function (parentNode, reference, userData) {

    assert(reference instanceof browse_service.ReferenceDescription);

    const crawler = this;

    let childCacheNodeRef = crawler._getCacheNode(reference.referenceTypeId);
    if (!childCacheNodeRef) {
        childCacheNodeRef = crawler._createCacheNode(reference.referenceTypeId);
        crawler._add_crawl_task(childCacheNodeRef, userData);
    }

    let childCacheNode = crawler._getCacheNode(reference.nodeId);
    if (!childCacheNode) {
        childCacheNode = crawler._createCacheNode(reference.nodeId);
        childCacheNode.browseName = reference.browseName;
        childCacheNode.displayName = reference.DisplayName;
        childCacheNode.typeDefinition = reference.typeDefinition;
        childCacheNode.nodeClass = reference.nodeClass;

        //xx console.log("HERE !",childCacheNode.toString());
        crawler._add_crawl_task(childCacheNode, userData);
    } else {

        if (userData.setExtraReference) {

            crawler._push_task("setExtraRef", {
                params: {
                    parentNode: parentNode,
                    reference: reference,
                    childCacheNode: childCacheNode,
                    userData: userData
                },
                func: _setExtraReference
            });
        }
    }
};

NodeCrawler.prototype.dispose = function () {
    const self = this;
    self.session = null;
    self.browseNameMap = null;
    self._nodesToReadEx = null;
    self._objectCache = null;
    self._objectToBrowse = null;
    self._objMap = null;
    self.q = null;
};

//---------------------------------------------------------------------------------------

NodeCrawler.prototype.read = function (nodeId, callback) {

    const self = this;

    try {
        nodeId = resolveNodeId(nodeId);
    } catch (err) {
        callback(err);
    }

    function remove_cycle(object, callback) {


        const visitedNodeIds = {};

        function hasBeenVisited(e) {
            const key = e.nodeId.toString();
            return visitedNodeIds[key];
        }

        function setVisited(e) {
            const key = e.nodeId.toString();
            return visitedNodeIds[key] = e;
        }

        function mark_array(arr) {
            if (!arr) {
                return;
            }
            assert(_.isArray(arr));
            for (let index = 0; index < arr.length; index++) {
                const e = arr[index];
                if (hasBeenVisited(e)) {
                    return;
                } else {
                    setVisited(e);
                    explorerObject(e);
                }
            }
        }

        function explorerObject(obj) {
            mark_array(obj.organizes);
            mark_array(obj.hasComponent);
            mark_array(obj.hasNotifier);
            mark_array(obj.hasProperty);
        }


        explorerObject(object);
        callback(null, object);
    }

    function simplify_object(objMap, object, final_callback) {

        assert(_.isFunction(final_callback));

        const queue = async.queue(function (task, callback) {

            setImmediate(function () {
                assert(_.isFunction(task.func));
                task.func(task.data, callback);
            });
        }, 1);

        const key = object.nodeId.toString();

        function add_for_reconstruction(object, extra_func) {
            assert(_.isFunction(extra_func));
            assert(typeof object.nodeId.toString() === "string");
            queue.push({
                data: object,
                func: function (data, callback) {

                    _reconstruct_manageable_object(data, function (err, obj) {
                        extra_func(err, obj);
                        callback(err);
                    });
                }
            });
        }

        function _reconstruct_manageable_object(object, callback) {

            assert(_.isFunction(callback));
            assert(object);
            assert(object.nodeId);

            const key = object.nodeId.toString();

            if (objMap.hasOwnProperty(key)) {
                return callback(null, objMap[key]);
            }
            /* reconstruct a more manageable object
             * var obj = {
             *    browseName: "Objects",
             *    organises : [
             *       {
             *            browseName: "Server",
             *            hasComponent: [
             *            ]
             *            hasProperty: [
             *            ]
             *       }
             *    ]
             * }
             */
            const obj = {
                browseName: object.browseName.name,
                nodeId: object.nodeId.toString()
            };

            // Append nodeClass
            if (object.nodeClass) {
                obj.nodeClass = object.nodeClass.toString();
            }
            if (object.dataType) {
                obj.dataType = object.dataType.toString();
                obj.dataTypeName = object.dataTypeName;
                //xx  console.log("dataTypeObj",object.dataTypeObj.browseName);
            }
            if (object.dataValue) {
                if (object.dataValue instanceof Array || object.dataValue.length > 10) {
                    // too much verbosity here
                } else {
                    obj.dataValue = object.dataValue.toString();
                }
            }
            objMap[key] = obj;

            const referenceMap = obj;

            object.references = object.references || [];


            object.references.map(function (ref) {

                assert(ref);
                const ref_index = ref.referenceTypeId.toString();

                const referenceType = self._objectCache[ref_index];

                if (!referenceType) {
                    console.log(("Unknown reference type " + ref_index).red.bold);
                    console.log(util.inspect(object, {colorize: true, depth: 10}));
                }
                const reference = self._objectCache[ref.nodeId.toString()];
                if (!reference) {
                    console.log(ref.nodeId.toString(), "bn=", ref.browseName.toString(), "class =", ref.nodeClass.toString(), ref.typeDefinition.toString());
                    console.log("#_reconstruct_manageable_object: Cannot find reference", ref.nodeId.toString(), "in cache");
                }
                // Extract nodeClass so it can be appended
                reference.nodeClass = ref.$nodeClass;

                const refName = lowerFirstLetter(referenceType.browseName.name);

                if (refName === "hasTypeDefinition") {
                    obj.typeDefinition = reference.browseName.name;
                } else {
                    if (!referenceMap[refName]) {
                        referenceMap[refName] = [];
                    }
                    add_for_reconstruction(reference, function (err, mobject) {
                        if (!err) {
                            referenceMap[refName].push(mobject);
                        }
                    });
                }
            });
            callback(null, obj);

        }

        add_for_reconstruction(object, function () {
        });

        queue.drain = function () {
            const object = self._objMap[key];
            remove_cycle(object, callback);
        };
    }

    const key = nodeId.toString();

    // check if object has already been crawled
    if (self._objMap.hasOwnProperty(key)) {
        const object = self._objMap[key];
        return callback(null, object);
    }

    const userData = {
        onBrowse: NodeCrawler.follow
    };


    self.crawl(nodeId, userData, function () {

        if (self._objectCache.hasOwnProperty(key)) {

            const cacheNode = self._objectCache[key];
            assert(cacheNode.browseName.name !== "pending");

            simplify_object(self._objMap, cacheNode, callback);

        } else {
            callback(new Error("Cannot find nodeid" + key));
        }
    });
};


exports.NodeCrawler = NodeCrawler;