node-opcua-factory
Version:
pure nodejs OPCUA SDK - module factory
623 lines (547 loc) • 21.7 kB
text/typescript
/* eslint-disable prefer-rest-params */
/* eslint-disable complexity */
/**
* @module node-opcua-factory
*/
// tslint:disable:no-shadowed-variable
import chalk from "chalk";
import { assert } from "node-opcua-assert";
import { AttributeIds } from "node-opcua-basic-types";
import { BinaryStream, BinaryStreamSizeCalculator, OutputBinaryStream } from "node-opcua-binary-stream";
import { hexDump, make_errorLog } from "node-opcua-debug";
import { NodeId } from "node-opcua-nodeid";
import { isNullOrUndefined } from "node-opcua-utils";
import { getBuiltInEnumeration, hasBuiltInEnumeration } from "./enumerations";
import { DataTypeFactory } from "./datatype_factory";
import { getStructureTypeConstructor } from "./get_standard_data_type_factory";
import {
EnumerationDefinition,
FieldCategory,
StructuredTypeField,
BuiltInTypeDefinition,
FieldType,
Func1,
IStructuredTypeSchema,
IBaseUAObject,
DecodeDebugOptions} from "./types";
const errorLog = make_errorLog(__filename);
function r(str: string, length = 30) {
return (str + " ").substring(0, length);
}
function _findFieldSchema(typeDictionary: DataTypeFactory, field: StructuredTypeField, value: any): IStructuredTypeSchema {
const fieldType = field.fieldType;
if (field.allowSubType && field.category === "complex") {
const fieldTypeConstructor = value ? value.constructor : field.fieldTypeConstructor;
const _newFieldSchema = fieldTypeConstructor.schema;
return _newFieldSchema as IStructuredTypeSchema;
}
const fieldTypeConstructor = field.fieldTypeConstructor;
if (fieldTypeConstructor) {
return fieldTypeConstructor.prototype.schema;
}
const strucutreInfo = typeDictionary.getStructureInfoByTypeName(fieldType);
return strucutreInfo.schema;
}
function _decode_member_(value: any, field: StructuredTypeField, stream: BinaryStream, options: DecodeDebugOptions) {
const tracer = options.tracer;
const cursorBefore = stream.length;
const fieldType = field.fieldType;
switch (field.category) {
case FieldCategory.basic:
if (field.schema.decode) {
value = field.schema.decode(stream);
}
tracer.trace("member", options.name, value, cursorBefore, stream.length, fieldType);
break;
case FieldCategory.enumeration:
if (field.schema.decode) {
value = field.schema.decode(stream);
}
tracer.trace("member", options.name, value, cursorBefore, stream.length, fieldType);
break;
case FieldCategory.complex: {
assert(field.category === FieldCategory.complex);
if (!field.fieldTypeConstructor) {
field.fieldTypeConstructor = getStructureTypeConstructor(field.fieldType);
}
if (typeof field.fieldTypeConstructor !== "function") {
throw new Error("Cannot find constructor for " + field.name + "of type " + field.fieldType);
}
// assert(typeof field.fieldTypeConstructor === "function");
const constructor = field.fieldTypeConstructor;
value = new constructor();
value.decodeDebug(stream, options);
}
}
return value;
}
function _applyOnAllSchemaFields<T>(self: BaseUAObject, schema: IStructuredTypeSchema, data: T, functor: Func1<T>, args?: any) {
const baseSchema = schema.getBaseSchema();
if (baseSchema) {
_applyOnAllSchemaFields(self, baseSchema, data, functor, args);
}
for (const field of schema.fields) {
functor(self, field, data, args);
}
}
const _nbElements = typeof process === "object" ? (process.env.ARRAYLENGTH ? parseInt(process.env.ARRAYLENGTH, 10) : 10) : 10;
const fullBuffer = typeof process === "object" ? !!process.env?.FULLBUFFER : false;
function _arrayEllipsis(value: any[] | null, data: ExploreParams): string {
if (!value) {
return "null []";
} else {
if (value.length === 0) {
return "[ /* empty*/ ]";
}
assert(Array.isArray(value));
const v = [];
const m = Math.min(_nbElements, value.length);
const ellipsis = value.length > _nbElements ? " ... " : "";
const pad = data.padding + " ";
let isMultiLine = true;
for (let i = 0; i < m; i++) {
let element = value[i];
if (element instanceof Buffer) {
element = hexDump(element, 32, 16);
} else if (isNullOrUndefined(element)) {
element = "null";
} else {
element = element.toString();
const s = element.split("\n");
if (s.length > 1) {
element = "\n" + pad + s.join("\n" + pad);
isMultiLine = true;
}
}
if (element.length > 80) {
isMultiLine = true;
}
v.push(element);
}
const length = "/* length =" + value.length + "*/";
if (isMultiLine) {
return "[ " + length + "\n" + pad + v.join(",\n" + pad + " ") + ellipsis + "\n" + data.padding + "]";
} else {
return "[ " + length + v.join(",") + ellipsis + "]";
}
}
}
interface ExploreParams {
padding: string;
lines: string[];
}
// eslint-disable-next-line complexity
// eslint-disable-next-line max-statements
function _exploreObject(self: BaseUAObject, field: StructuredTypeField, data: ExploreParams, args: any): void {
if (!self) {
return;
}
const fieldType = field.fieldType;
const fieldName = field.name;
const category = field.category;
const padding = data.padding;
let value = (self as any)[fieldName];
let str;
// decorate the field name with ?# if the field is optional
let opt = " ";
if (field.switchBit !== undefined) {
opt = " ?" + field.switchBit + " ";
}
if (field.switchValue !== undefined) {
opt = " !" + field.switchValue + " ";
}
const allowSubTypeSymbol = field.allowSubType ? "~" : " ";
const arraySymbol = field.isArray ? "[]" : " ";
const fieldNameF = chalk.yellow(r(padding + fieldName, 30));
const fieldTypeF = chalk.cyan(`/* ${allowSubTypeSymbol}${r(fieldType + opt, 38)}${arraySymbol} */`);
// detected when optional field is not specified in value
if (field.switchBit !== undefined && value === undefined) {
str = fieldNameF + " " + fieldTypeF + ": " + chalk.italic.grey("undefined") + " /* optional field not specified */";
data.lines.push(str);
return;
}
// detected when union field is not specified in value
if (field.switchValue !== undefined && value === undefined) {
str = fieldNameF + " " + fieldTypeF + ": " + chalk.italic.grey("undefined") + " /* union field not specified */";
data.lines.push(str);
return;
}
// compact version of very usual objects
if (fieldType === "QualifiedName" && !field.isArray && value) {
value = value.toString() || "<null>";
str = fieldNameF + " " + fieldTypeF + ": " + chalk.green(value.toString());
data.lines.push(str);
return;
}
if (fieldType === "LocalizedText" && !field.isArray && value) {
value = value.toString() || "<null>";
str = fieldNameF + " " + fieldTypeF + ": " + chalk.green(value.toString());
data.lines.push(str);
return;
}
if (fieldType === "DataValue" && !field.isArray && value) {
value = value.toString(data);
str = fieldNameF + " " + fieldTypeF + ": " + chalk.green(value.toString(data));
data.lines.push(str);
return;
}
if(fieldType === "DiagnosticInfo" && !field.isArray && value) {
value = value.toString(data);
str = fieldNameF + " " + fieldTypeF + ": " + chalk.green(value.toString(data));
data.lines.push(str);
return;
}
function _dump_enumeration_value(
self: BaseUAObject,
field: StructuredTypeField,
data: ExploreParams,
value: any,
fieldType: string
) {
const s = field.schema as EnumerationDefinition;
// istanbul ignore next
if (!s.typedEnum) {
// tslint:disable:no-console
errorLog("xxxx cannot find typeEnum", s);
}
const convert = (value: number) => {
// istanbul ignore next
if (!s.typedEnum.get(value)) {
return [value, s.typedEnum.get(value)] as [number, any];
} else {
return [value, s.typedEnum.get(value)!.key] as [number, any];
}
};
const toS = ([n, s]: [number, any]) => `${n} /*(${s})*/`;
if (field.isArray) {
str =
fieldNameF +
" " +
fieldTypeF +
": [" +
value
.map((c: number) => convert(c))
.map(toS)
.join(", ") +
"]";
data.lines.push(str);
} else {
const c = convert(value);
str = `${fieldNameF} ${fieldTypeF}: ${toS(c)}`;
data.lines.push(str);
}
}
function _dump_simple_value(
self: BaseUAObject,
field: StructuredTypeField,
data: ExploreParams,
value: any,
fieldType: string
) {
let str = "";
if (value instanceof Buffer) {
data.lines.push(fieldNameF + " " + fieldTypeF);
if (fullBuffer || value.length <= 32) {
const _hexDump = value.length <= 32 ? "Ox" + value.toString("hex") : "\n" + hexDump(value);
data.lines.push("Buffer: " + _hexDump);
} else {
const _hexDump1 = value.subarray(0, 16).toString("hex");
const _hexDump2 = value.subarray(-16).toString("hex");
data.lines.push("Buffer: ", _hexDump1 + "..." + _hexDump2);
}
} else {
if (field.isArray) {
str = fieldNameF + " " + fieldTypeF + ": " + _arrayEllipsis(value, data);
} else {
if (field.fieldType === "NodeId" && value instanceof NodeId) {
value = value.displayText();
} else if (fieldType === "IntegerId" || fieldType === "UInt32") {
if (field.name === "attributeId") {
value = "AttributeIds." + AttributeIds[value] + "/* " + value + " */";
} else {
const extra = value !== undefined ? "0x" + value.toString(16) : "undefined";
value = "" + value + " " + extra;
}
} else if (fieldType === "DateTime" || fieldType === "UtcTime") {
try {
value = value && value.toISOString ? value.toISOString() : value;
} catch {
value = chalk.red(value?.toString() + " *** ERROR ***");
}
} else if (typeof value === "object" && value !== null && value !== undefined) {
// eslint-disable-next-line prefer-spread
value = value.toString.apply(value, args);
}
str =
fieldNameF +
" " +
fieldTypeF +
": " +
(value === null || value === undefined ? chalk.blue("null") : value.toString());
}
data.lines.push(str);
}
}
function _dump_complex_value(
self: BaseUAObject,
field: StructuredTypeField,
data: ExploreParams,
value: any,
fieldType: string
) {
if (field.subType) {
// this is a synonymous
fieldType = field.subType;
_dump_simple_value(self, field, data, value, fieldType);
} else {
const typeDictionary = self.schema.getDataTypeFactory();
// istanbul ignore next
if (!typeDictionary) {
errorLog("Internal Error: No typeDictionary for ", self.schema);
return;
}
if (field.isArray) {
if (value === null) {
data.lines.push(fieldNameF + " " + fieldTypeF + ": null []");
} else if (value.length === 0) {
data.lines.push(fieldNameF + " " + fieldTypeF + ": [ /* empty */ ]");
} else {
data.lines.push(fieldNameF + " " + fieldTypeF + ": [");
const m = Math.min(_nbElements, value.length);
for (let i = 0; i < m; i++) {
const element = value[i];
const _newFieldSchema = _findFieldSchema(typeDictionary, field, element);
data.lines.push(padding + ` { ` + chalk.cyan(`/* ${i} - ${_newFieldSchema?.name}*/`));
const data1 = {
lines: [] as string[],
padding: padding + " "
};
_applyOnAllSchemaFields(element, _newFieldSchema, data1, _exploreObject, args);
data.lines = data.lines.concat(data1.lines);
data.lines.push(padding + " }" + (i === value.length - 1 ? "" : ","));
}
if (m < value.length) {
data.lines.push(padding + " ..... ( " + value.length + " elements )");
}
data.lines.push(padding + "]");
}
} else {
const _newFieldSchema = _findFieldSchema(typeDictionary, field, value);
data.lines.push(fieldNameF + " " + fieldTypeF + ": {");
const data1 = { padding: padding + " ", lines: [] as string[] };
_applyOnAllSchemaFields(value, _newFieldSchema, data1, _exploreObject, args);
data.lines = data.lines.concat(data1.lines);
data.lines.push(padding + "}");
}
}
}
switch (category) {
case FieldCategory.enumeration:
_dump_enumeration_value(self, field, data, value, fieldType);
break;
case FieldCategory.basic:
_dump_simple_value(self, field, data, value, fieldType);
break;
case FieldCategory.complex:
_dump_complex_value(self, field, data, value, fieldType);
break;
default:
throw new Error("internal error: unknown kind_of_field " + category);
}
}
function json_ify(t: BuiltInTypeDefinition, value: any, fieldType: FieldType) {
if (value instanceof Array) {
return value.map((e) => (e && e.toJSON ? e.toJSON() : e));
}
/*
if (typeof fieldType.toJSON === "function") {
return fieldType.toJSON(value);
} else
*/
if (t && t.toJSON) {
return t.toJSON(value);
} else if (value?.toJSON) {
return value.toJSON();
} else {
return value;
}
}
function _JSONify(self: BaseUAObject, schema: IStructuredTypeSchema, pojo: any) {
/* jshint validthis: true */
for (const field of schema.fields) {
const fieldValue = (self as any)[field.name];
if (fieldValue === null || fieldValue === undefined) {
continue;
}
if (hasBuiltInEnumeration(field.fieldType)) {
const enumeration = getBuiltInEnumeration(field.fieldType);
assert(enumeration !== null);
if (field.isArray) {
pojo[field.name] = fieldValue.map((value: any) => enumeration.enumValues[value.toString()]);
} else {
pojo[field.name] = enumeration.enumValues[fieldValue.toString()];
}
continue;
}
const t = field.schema as BuiltInTypeDefinition; // getBuiltInType(field.fieldType);
if (field.isArray) {
pojo[field.name] = fieldValue.map((value: any) => json_ify(t, value, field));
} else {
pojo[field.name] = json_ify(t, fieldValue, field);
}
}
}
export interface BaseUAObject extends IBaseUAObject {
schema: IStructuredTypeSchema;
}
/**
* base class for all OPCUA objects
*/
export class BaseUAObject {
constructor() {
/** */
}
/**
* Encode the object to the binary stream.
*/
public encode(stream: OutputBinaryStream): void {
/** */
}
/**
* Decode the object from the binary stream.
*/
public decode(stream: BinaryStream): void {
/** */
}
/**
* Calculate the required size to store this object in a binary stream.
*/
public binaryStoreSize(): number {
const stream = new BinaryStreamSizeCalculator();
this.encode(stream);
return stream.length;
}
/**
*/
public toString(...args: any[]): string {
if (this.schema && Object.prototype.hasOwnProperty.call(this.schema, "toString")) {
return this.schema.toString.apply(this, arguments as any);
} else {
if (!this.explore) {
return Object.prototype.toString.apply(this, arguments as any);
}
return this.explore();
}
}
/**
*
* verify that all object attributes values are valid according to schema
*/
public isValid(): boolean {
assert(this.schema);
if (this.schema.isValid) {
return this.schema.isValid(this);
} else {
return true;
}
}
/**
*
*/
public decodeDebug(stream: BinaryStream, options: DecodeDebugOptions): void {
const tracer = options.tracer;
const schema = this.schema;
tracer.trace("start", options.name + "(" + schema.name + ")", stream.length, stream.length);
const self: any = this as any;
for (const field of schema.fields) {
const value = self[field.name];
if (typeof field.switchValue === "number") {
// skip
if (self["switchField"] !== field.switchValue) {
continue;
}
}
if (field.isArray) {
const cursorBefore = stream.length;
let nb = stream.readUInt32();
if (nb === 0xffffffff) {
nb = 0;
}
options.name = field.name + [];
tracer.trace("start_array", field.name, nb, cursorBefore, stream.length);
for (let i = 0; i < nb; i++) {
tracer.trace("start_element", field.name, i);
options.name = "element #" + i;
_decode_member_(value, field, stream, options);
tracer.trace("end_element", field.name, i);
}
tracer.trace("end_array", field.name, stream.length - 4);
} else {
options.name = field.name;
_decode_member_(value, field, stream, options);
}
}
tracer.trace("end", schema.name, stream.length, stream.length);
}
public explore(): string {
const data: { padding: string; lines: string[] } = {
lines: [],
padding: " "
};
data.lines.push("{" + chalk.cyan(" /*" + (this.schema ? this.schema.name : "") + "*/"));
if (this.schema) {
this.applyOnAllFields(_exploreObject, data);
}
data.lines.push("};");
return data.lines.join("\n");
}
public applyOnAllFields<T>(func: Func1<T>, data: T): void {
_applyOnAllSchemaFields(this, this.schema, data, func, null);
}
public toJSON(): any {
assert(this.schema);
if (this.schema?.toJSON) {
return this.schema.toJSON.apply(this, arguments as any);
} else {
assert(this.schema);
const schema = this.schema;
const pojo = {};
_visitSchemaChain(this, schema, pojo, _JSONify, null);
return pojo;
}
}
public clone(/*options,optionalFilter,extraInfo*/): any {
const self: any = this as any;
const params = {};
function construct_param(schema: any, options: any) {
for (const field of schema.fields) {
const f = self[field.name];
if (f === null || f === undefined) {
continue;
}
if (field.isArray) {
options[field.name] = [...self[field.name]];
} else {
options[field.name] = self[field.name];
}
}
}
construct_param.call(this, self.schema, params);
return new self.constructor(params);
}
}
function _visitSchemaChain(
self: BaseUAObject,
schema: IStructuredTypeSchema,
pojo: any,
func: (self: BaseUAObject, schema: IStructuredTypeSchema, pojo: any) => void,
extraData: any
) {
assert(typeof func === "function");
// apply also construct to baseType schema first
const baseSchema = schema.getBaseSchema ? schema.getBaseSchema() : null;
if (baseSchema) {
_visitSchemaChain(self, baseSchema, pojo, func, extraData);
}
func.call(null, self, schema, pojo);
}