"use strict";
const assert = require("node-opcua-assert").assert;
const _ = require("underscore");
const path = require("path");
const fs = require("fs");
const resolveNodeId = require("node-opcua-nodeid").resolveNodeId;
const AddressSpace = require("node-opcua-address-space").AddressSpace;
const UADataType = require("node-opcua-address-space").UADataType;
const normalize_require_file = require("node-opcua-utils").normalize_require_file;
const LineFile = require("node-opcua-utils").LineFile;
const lowerFirstLetter = require("node-opcua-utils").lowerFirstLetter;
const hasConstructor = require("node-opcua-factory").hasConstructor;
const getConstructor = require("node-opcua-factory").getConstructor;
const hasEnumeration = require("node-opcua-factory").hasEnumeration;
const getEnumeration = require("node-opcua-factory").getEnumeration;
const crypto = require("crypto");
function hashNamespace(namespaceUri) {
const hash = crypto
.createHash("sha1")
.update(namespaceUri)
.digest("hex");
return hash;
}
/**
* returns the location of the javascript version of the schema corresponding to schemaName
* @method getSchemaSourceFile
* @param namespace {String}
* @param schemaName {String}
* @param schema_type {String} "enum" | "schema"
* @param schema_folder {String}
* @return {string}
* @private
*/
function getSchemaSourceFile(namespace, schemaName, schema_type, schema_folder) {
assert(schemaName.match(/[a-zA-Z]+/));
if (!fs.existsSync(schema_folder)) {
throw new Error("Cannot find schema folder " + schema_folder);
}
if (!(schema_type === "enum" || schema_type === "schema" || schema_type === "")) {
throw new Error(" unexpected schema_type" + schema_type);
}
const subfolder = hashNamespace(namespace);
const root = path.join(schema_folder, "schemas");
const folder = path.normalize(path.join(root, subfolder));
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder);
}
if (schema_type === "") {
return path.join(folder, schemaName + ".js");
} else {
return path.join(folder, schemaName + "_" + schema_type + ".js");
}
}
/**
* convert a nodeset enumeration into a javascript script enumeration code
* @method generateEnumerationCode
* @param dataType
* @param filename {string} the output filename
*
*/
function generateEnumerationCode(dataType, filename) {
assert(typeof filename === "string");
const dataTypeName = dataType.browseName.name.toString();
assert(!hasEnumeration(dataTypeName));
// create the enumeration file
const f = new LineFile();
f.write("// namespace " + dataType.namespaceUri.toString());
f.write('const factories = require("node-opcua-factory");');
f.write('const makeNodeId = require("node-opcua-nodeid").makeNodeId;');
f.write("const " + dataTypeName + "_Schema = {");
f.write(" id: makeNodeId(" + dataType.nodeId.value + "," + dataType.nodeId.namespace + "),");
f.write(" name: '" + dataTypeName + "',");
f.write(" namespace: '" + dataType.nodeId.namespace + "',");
f.write(" enumValues: {");
dataType.definition.forEach(function(pair) {
f.write(" " + pair.name + ": " + pair.value + ",");
});
f.write(" }");
f.write("};");
f.write("exports." + dataTypeName + "_Schema = " + dataTypeName + "_Schema;");
f.write("exports." + dataTypeName + " = factories.registerEnumeration(" + dataTypeName + "_Schema);");
f.save(filename);
}
const QualifiedName = require("node-opcua-data-model").QualifiedName;
/**
* const dataType = {
* browseName: "Color",
* definition: [
* { name: "Red", value: 12},
* { name: "Blue", value: 11}
* ]
* };
*
* makeEnumeration(dataType);
*
* @method makeEnumeration
*
* @param dataType {Object}
* @return {*}
*/
function makeEnumeration(dataType, bForce) {
assert(dataType);
assert(dataType.hasOwnProperty("browseName"));
assert(dataType.browseName instanceof QualifiedName);
assert(_.isArray(dataType.definition));
const dataTypeName = dataType.browseName.name.toString();
if (hasEnumeration(dataTypeName)) {
return getEnumeration(dataTypeName).typedEnum;
}
//const Enumeration_Schema = {
// id: dataType.nodeId,
// name: dataType.browseName.toString(),
// enumValues: {}
//};
//
//dataType.definition.forEach(function (pair) {
// Enumeration_Schema.enumValues[pair.name] = parseInt(pair.value, 10);
//});
const namespace = dataType.namespaceUri;
const filename = getSchemaSourceFile(namespace, dataType.browseName.toString(), "enum");
generateEnumerationCode(dataType, filename);
const relative_filename = normalize_require_file(__dirname, filename);
return require(relative_filename)[dataType.browseName.toString()];
}
exports.makeEnumeration = makeEnumeration;
function generateStructureCode(namespace, schema, schema_folder) {
assert(fs.existsSync(schema_folder, "schema folder must exist"));
const name = schema.name;
const f = new LineFile();
f.write('const factories = require("node-opcua-factory");');
f.write('const coerceNodeId = require("node-opcua-nodeid").coerceNodeId;');
f.write("const " + schema.name + "_Schema = {");
f.write(" id: coerceNodeId('" + schema.id.toString() + "'),");
f.write(' name: "' + name + '",');
f.write(" fields: [");
schema.fields.forEach(function(field) {
f.write(" {");
f.write(' name: "' + field.name + '",');
f.write(' fieldType: "' + field.fieldType + '"');
if (field.isArray) {
f.write(" , isArray:" + (field.isArray ? "true" : false));
}
if (field.description) {
f.write(" , documentation:" + ' "' + field.description + '" ');
}
f.write(" },");
});
f.write(" ]");
f.write(" };");
f.write("exports." + name + "_Schema = " + name + "_Schema;");
//xx write("exports."+name+" = factories.registerObject(" + name+"_Schema);");
const filename = getSchemaSourceFile(namespace, name, "schema", schema_folder);
f.save(filename);
}
function generateFileCode(namespace, schema, schema_folder) {
assert(fs.existsSync(schema_folder, "schema folder must exist"));
assert(typeof namespace === "string");
const f = new LineFile();
const name = schema.name;
const hint = "$node-opcua/schemas/" + hashNamespace(namespace);
f.write("// namespace " + namespace.toString());
f.write('const registerObject = require("node-opcua-factory").registerObject;');
// f.write("registerObject('_generated_schemas|"+ name + "','_generated_schemas');");
f.write("registerObject('" + hint + "|" + name + "');");
//
f.write('require("' + hint + "/" + name + '_schema");');
// const filename = "../_generated_schemas/_auto_generated_"+ name;
let filename = "$node-opcua/generated/_auto_generated_" + name;
f.write("const " + name + ' = require("' + filename + '").' + name + ";");
f.write("exports." + name + " = " + name + ";");
filename = getSchemaSourceFile(namespace, name, "", schema_folder);
f.save(filename);
}
function makeStructure(dataType, bForce, schema_folder) {
assert(fs.existsSync(schema_folder), " schema_folder must exist");
bForce = !!bForce;
assert(dataType instanceof UADataType);
const addressSpace = dataType.addressSpace;
assert(addressSpace.constructor.name === "AddressSpace");
assert(addressSpace instanceof AddressSpace);
const namespaceUri = addressSpace.getNamespaceUri(dataType.nodeId.namespace);
// istanbul ignore next
if (!dataType.binaryEncodingNodeId) {
throw new Error(
"DataType with name " +
dataType.browseName.toString() +
" has no binaryEncoding node\nplease check your nodeset file"
);
}
// if binaryEncodingNodeId is in the standard factory => no need to overwrite
if (!bForce && (hasConstructor(dataType.binaryEncodingNodeId) || dataType.binaryEncodingNodeId.namespace === 0)) {
//xx console.log("Skipping standard constructor".bgYellow ," for dataType" ,dataType.browseName.toString());
return getConstructor(dataType.binaryEncodingNodeId);
}
const schema = constructSchema(addressSpace, dataType);
generateFileCode(namespaceUri, schema, schema_folder);
generateStructureCode(namespaceUri, schema);
const filename = getSchemaSourceFile(namespaceUri, schema.name, "");
const relative_filename = normalize_require_file(__dirname, filename);
//xx console.log("xxxxxxxxxxxxxxxxxx => ".green,schema.name,filename.cyan,relative_filename.yellow);
const constructor = require(relative_filename)[schema.name];
assert(_.isFunction(constructor), "expecting a constructor here");
return constructor;
}
exports.makeStructure = makeStructure;
/*= private
*
* @example:
* @example:
* const dataType = {
* browseName: "ServerStatusDataType",
* definition: [
* { name "timeout", dataType: "UInt32" }
* ]
* };
* @param dataType {Object}
* @return {*}
*/
function constructSchema(addressSpace, dataType) {
let dataTypeName = dataType.browseName.name.toString();
// remove DataType to get the name of the class
dataTypeName = dataTypeName.replace(/DataType/, "");
const schema = {
id: dataType.binaryEncodingNodeId,
name: dataTypeName,
namespace: dataType.nodeId.namespace,
fields: [
// { name: "title", fieldType: "UAString" , isArray: false , documentation: "some text"},
]
};
const enumeration = addressSpace.findDataType("Enumeration");
assert(enumeration, "Enumeration Type not found: please check your nodeset file");
const structure = addressSpace.findDataType("Structure");
assert(structure, "Structure Type not found: please check your nodeset file");
// construct the fields
dataType.definition.forEach(function(pair) {
const dataTypeId = resolveNodeId(pair.dataType);
const fieldDataType = addressSpace.findNode(dataTypeId);
if (!fieldDataType) {
throw new Error(
" cannot find description for object " +
dataTypeId +
". Check that this node exists in the nodeset.xml file"
);
}
//xx console.log("xxxxx dataType",dataType.toString());
// check if dataType is an enumeration or a structure or a basic type
if (fieldDataType.isSupertypeOf(enumeration)) {
makeEnumeration(fieldDataType);
}
if (fieldDataType.isSupertypeOf(structure)) {
makeStructure(fieldDataType);
}
let dataTypeName = fieldDataType.browseName.toString();
dataTypeName = dataTypeName.replace(/DataType/, "");
schema.fields.push({
name: lowerFirstLetter(pair.name),
fieldType: dataTypeName,
isArray: false,
description: pair.description ? pair.description.text : ""
});
});
return schema;
}
const nodeset = {
ServerState: null,
ServerStatus: null,
ServiceCounter: null,
SessionDiagnostics: null
};
exports.nodeset = nodeset;
function registerDataTypeEnum(addressSpace, dataTypeName, bForce) {
const dataType = addressSpace.findDataType(dataTypeName);
assert(dataType);
const superType = addressSpace.findNode(dataType.subtypeOf);
assert(superType.browseName.toString() === "Enumeration");
return makeEnumeration(dataType, bForce);
}
function registerDataType(addressSpace, dataTypeName, schema_folder, bForce) {
if (!fs.existsSync(schema_folder)) {
throw new Error("schema_folder must exist : " + dataTypeName + " " + schema_folder);
}
let dataType = addressSpace.findDataType(dataTypeName + "DataType");
if (!dataType) {
dataType = addressSpace.findDataType(dataTypeName);
}
// istanbul ignore next
if (!dataType) {
console.log("registerDataType: warning : Cannot find DataType " + dataTypeName);
return null;
//xx throw new Error(" Cannot find DataType " + dataTypeName);
}
const superType = addressSpace.findNode(dataType.subtypeOf);
assert(superType.browseName.toString() === "Structure");
// finding object with encoding
//
// <UAObject NodeId="i=864" BrowseName="Default Binary" SymbolicName="DefaultBinary">
// <DisplayName>Default Binary</DisplayName>
// <References>
// <Reference ReferenceType="HasEncoding" IsForward="false">i=862</Reference>
return makeStructure(dataType, bForce, schema_folder);
}
/**
* creates the requested data structure and javascript objects for the OPCUA objects
* @method createExtensionObjectDefinition
* @param addressSpace {AddressSpace}
*/
const createExtensionObjectDefinition = function(addressSpace) {
assert(addressSpace instanceof AddressSpace);
const force = true;
// nodeset.ApplicationDescription = nodeset.ApplicationDescription || registerDataType(addressSpace, "ApplicationDescription",force);
nodeset.ServerState = nodeset.ServerState || registerDataTypeEnum(addressSpace, "ServerState", force);
nodeset.ServerStatus = nodeset.ServerStatus || registerDataType(addressSpace, "ServerStatus", force);
//xx nodeset.ServiceCounter = nodeset.ServiceCounter || registerDataType(addressSpace, "ServiceCounter", force);
//xx nodeset.SessionDiagnostics = nodeset.SessionDiagnostics || registerDataType(addressSpace, "SessionDiagnostics",force);
//xx nodeset.ServerDiagnosticsSummary = nodeset.ServerDiagnosticsSummary || registerDataType(addressSpace, "ServerDiagnosticsSummary",force);
//xx nodeset.SubscriptionDiagnostics = nodeset.SubscriptionDiagnostics || registerDataType(addressSpace,"SubscriptionDiagnostics",force);
//xx nodeset.ModelChangeStructure = nodeset.ModelChangeStructure || registerDataType(addressSpace,"ModelChangeStructure",force);
//xx nodeset.SemanticChangeStructure = nodeset.SemanticChangeStructure || registerDataType(addressSpace,"SemanticChangeStructure",force);
//xx nodeset.RedundantServer = nodeset.RedundantServer || registerDataType(addressSpace,"RedundantServer",force);
//xx nodeset.SamplingIntervalDiagnostics = nodeset.SamplingIntervalDiagnostics || registerDataType(addressSpace,"SamplingIntervalDiagnostics",force);
//xx nodeset.SessionSecurityDiagnostics = nodeset.SessionSecurityDiagnostics || registerDataType(addressSpace,"SessionSecurityDiagnostics",force);
/*
* This Structured DataType defines the local time that may or may not take daylight saving time
* into account. Its elements are described in Table 24.
* Table 24 – TimeZoneDataType Definition
* Name Type Description
* TimeZoneDataType structure
* offset Int16 The offset in minutes from UtcTime
* daylightSavingInOffset Boolean If TRUE, then daylight saving time (DST) is in effect and offset
* includes the DST correction. If FALSE then the offset does not
* include the DST correction and DST may or may not have
* been in effect.
*/
//xx nodeset.TimeZoneDataType = nodeset.TimeZoneDataType || registerDataType(addressSpace, "TimeZoneDataType", force);
};
exports.createExtensionObjectDefinition = createExtensionObjectDefinition;