/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using Microsoft.Extensions.Logging;
using Opc.Ua.Export;
using Opc.Ua.Schema.Types;
using Opc.Ua.SourceGeneration;
using Opc.Ua.Types;
namespace Opc.Ua.Schema.Model
{
///
/// Generates files used to describe data types.
///
public class ModelDesignValidator : SchemaValidator, IModelDesign
{
///
/// Create model design validator
///
public ModelDesignValidator(
IFileSystem fileSystem,
uint startId,
IReadOnlyList exclusions,
ITelemetryContext telemetry,
SpecificationVersion standardVersion)
: base(fileSystem, null, null)
{
m_telemetry = telemetry;
m_logger = telemetry.CreateLogger();
m_context = ServiceMessageContext.CreateEmpty(telemetry);
m_startId = startId;
m_exclusions = exclusions;
m_standardVersion = standardVersion;
}
///
public IEnumerable Dependencies => m_dependencies.Values;
///
public Namespace[] Namespaces => m_dictionary?.Namespaces;
///
public Namespace TargetNamespace { get; private set; }
///
public DateTime? TargetPublicationDate =>
m_dictionary.TargetPublicationDateSpecified &&
m_dictionary.TargetPublicationDate != DateTime.MinValue ?
m_dictionary.TargetPublicationDate : null;
///
public string TargetVersion => m_dictionary.TargetVersion;
///
public bool UseAllowSubtypes { get; set; }
///
public NamespaceTable NamespaceUris { get; private set; }
///
public NodeDesign[] Nodes => m_dictionary.Items;
///
public IReadOnlyDictionary RolePermissions
=> m_defaultRolePermissions;
///
public IReadOnlyDictionary AccessRestrictions
=> m_defaultAccessRestrictions;
///
/// Max recursion
///
public int MaxRecursionDepth { get; set; } = 100;
///
/// Is Release candidate
///
public bool ReleaseCandidate { get; set; }
///
/// ModelVersion
///
public string ModelVersion { get; set; }
///
/// ModelPublicationDate
///
public string ModelPublicationDate { get; set; }
///
/// Returns a list of services filter by their service category.
///
public Service[] GetListOfServices(params ServiceCategory[] serviceCategories)
{
return [.. m_nodes.Values
.OfType()
.Where(x =>
!IsExcluded(x) &&
x.Service != null &&
(
serviceCategories == null ||
serviceCategories.Length == 0 ||
serviceCategories.Contains(x.Service.Category)
))
.Select(x => x.Service)
.Distinct()];
}
///
/// Returns a list of nodes in the design that should be
/// used to generate code for
///
public IEnumerable GetNodeDesigns()
{
foreach (NodeDesign node in m_dictionary.Items)
{
// Fiter nodes that are only declared but not part
// of code generation
if (!IsExcluded(node) && !node.IsDeclaration)
{
yield return node;
}
}
}
///
/// Validates the specified target design files and identifier file, using
/// the supplied solely for cross-model
/// reference resolution (dependencies are never code-generated).
///
///
/// Design files to generate code for.
/// Must contain at least one entry; the first entry is the primary
/// model paired with .
/// Design files providing namespace
/// resolution only (e.g. companion specs referenced by the targets).
/// May be empty or null.
/// Optional CSV identifier file
/// associated with the primary target.
public void Validate(
IReadOnlyList targetFilePaths,
IReadOnlyList dependencyFilePaths,
string identifierFilePath)
{
if (targetFilePaths == null || targetFilePaths.Count == 0)
{
throw new ArgumentException(
"No target design files specified",
nameof(targetFilePaths));
}
dependencyFilePaths ??= [];
// Stack generation fast path: StandardTypes + UACoreServices are
// the two halves of the core UA model; they are both generated
// from a combined target model.
if (targetFilePaths.Count == 2 &&
dependencyFilePaths.Count == 0 &&
targetFilePaths[0] == BuiltInDesignFiles.StandardTypesXml &&
targetFilePaths[1] == BuiltInDesignFiles.UACoreServicesXml &&
identifierFilePath == BuiltInDesignFiles.StandardTypesCsv)
{
ValidateCoreModel(targetFilePaths, identifierFilePath);
return;
}
// Internal representation keeps the primary target first, followed
// by remaining targets, followed by dependencies. This preserves
// existing ValidateModel/GetNamespaceList semantics: index 0 is the
// model that gets generated; everything else contributes to
// namespace/node resolution only.
var allFilePaths = new List(
targetFilePaths.Count + dependencyFilePaths.Count);
allFilePaths.AddRange(targetFilePaths);
allFilePaths.AddRange(dependencyFilePaths);
ValidateModel(allFilePaths, identifierFilePath);
}
///
/// Is excluded design
///
///
public bool IsExcluded(NodeDesign node)
{
if (node == null)
{
return false;
}
if (node.Purpose == DataTypePurpose.Testing)
{
return true;
}
if (m_exclusions != null)
{
foreach (string exclusion in m_exclusions)
{
if (exclusion == node.ReleaseStatus.ToString())
{
return true;
}
if (exclusion == node.Purpose.ToString())
{
return true;
}
if (node.Category != null &&
node.Category.Contains(exclusion, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
///
/// Is excluded data type
///
public bool IsExcluded(DataType dataType)
{
if (m_exclusions != null)
{
foreach (string exclusion in m_exclusions)
{
if (exclusion == dataType.ReleaseStatus.ToString())
{
return true;
}
if (exclusion == dataType.Purpose.ToString())
{
return true;
}
if (dataType.Category != null &&
dataType.Category.Contains(exclusion, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
///
/// Is excluded field
///
public bool IsExcluded(Parameter field)
{
if (field == null)
{
return false;
}
if (m_exclusions != null)
{
foreach (string exclusion in m_exclusions)
{
if (exclusion == field.ReleaseStatus.ToString())
{
return true;
}
}
}
return false;
}
///
/// Find node design
///
public bool TryFindNode(
XmlQualifiedName symbolicId,
string sourceName,
string referenceName,
out NodeDesign target)
{
if (IsNull(symbolicId))
{
throw Exception(
"The {0} reference for node is missing: {1}.",
referenceName,
sourceName);
}
return m_nodes.TryGetValue(symbolicId, out target);
}
///
/// Validate model
///
///
///
private void ValidateModel(
List designFilePaths,
string identifierFilePath)
{
string inputPath = designFilePaths[0];
// initialize tables.
m_identifiers = [];
m_nodes = [];
m_namespaceTables = [];
m_nodesByNodeId = [];
m_browseNames = [];
m_designFilePaths = [];
LoadBuiltInModel();
m_designFilePaths[Ua.Types.Namespaces.OpcUa] = string.Empty;
// load the design files.
List namespaces = GetNamespaceList(designFilePaths);
// The primary target (designFilePaths[0]) is loaded separately
// below. It is identified by its FilePath, not by list index,
// because GetNamespaceList's reordering (which propagates target
// namespaces ahead of the namespaces they reference) is allowed
// to move a dependency that imports the primary target ahead of
// it. Skipping purely by "ii > 0" would then double-load the
// primary target as a dependency.
for (int ii = namespaces.Count - 1; ii >= 0; ii--)
{
if (namespaces[ii].FilePath == null ||
namespaces[ii].FilePath == inputPath)
{
continue;
}
ModelDesign dependency = LoadDesignFile(
namespaces,
namespaces[ii].FilePath,
null,
validateDictionary: false);
if (dependency.Namespaces != null)
{
Namespace ns = dependency.Namespaces
.FirstOrDefault(x => x.Value == dependency.TargetNamespace);
namespaces[ii].Name = ns.Name;
namespaces[ii].Prefix = ns.Prefix;
namespaces[ii].XmlPrefix = ns.XmlPrefix;
}
}
ModelDesign targetModel = LoadDesignFile(
namespaces,
designFilePaths[0],
identifierFilePath);
// set a default xml namespace.
if (string.IsNullOrEmpty(targetModel.TargetXmlNamespace))
{
targetModel.TargetXmlNamespace = targetModel.TargetNamespace;
if (!string.IsNullOrEmpty(ModelVersion))
{
targetModel.TargetVersion = ModelVersion;
}
if (!string.IsNullOrEmpty(ModelPublicationDate))
{
var dt = DateTime.Parse(
ModelPublicationDate,
CultureInfo.InvariantCulture,
DateTimeStyles.None);
targetModel.TargetPublicationDate = new DateTime(
dt.Year,
dt.Month,
dt.Day,
0,
0,
0,
DateTimeKind.Utc);
targetModel.TargetPublicationDateSpecified = true;
}
}
// mark the target namespace as found.
m_designFilePaths[targetModel.TargetNamespace] = inputPath;
// load any included design files.
if (targetModel.Namespaces != null)
{
m_dependencies = [];
for (int ii = 0; ii < targetModel.Namespaces.Length; ii++)
{
Namespace ns = targetModel.Namespaces[ii];
if (ns.Value != targetModel.TargetNamespace)
{
Namespace dependency = namespaces
.FirstOrDefault(x => x.Value == ns.Value);
if (dependency != null)
{
ns.Name = dependency.Name;
ns.Prefix = dependency.Prefix;
ns.XmlPrefix = dependency.XmlPrefix;
ns.FilePath = dependency.FilePath;
}
DateTime? pd = ns.PublicationDate != null ?
DateTime.Parse(
ns.PublicationDate,
CultureInfo.InvariantCulture,
DateTimeStyles.None) :
null;
m_dependencies[ns.Value] = new ModelTableEntry
{
ModelUri = ns.Value,
XmlSchemaUri = ns.XmlNamespace,
Version = ns.Version,
ModelVersion = CoreUtils.FixupAsSemanticVersion(ns.Version),
PublicationDate = pd ?? DateTime.MinValue,
PublicationDateSpecified = pd != null
};
}
// override the publication date.
if (ns.PublicationDate == null && ns.Version == null)
{
continue;
}
if (!m_dependencies.TryGetValue(ns.Value, out ModelTableEntry modelInfo))
{
continue;
}
if (!string.IsNullOrWhiteSpace(ns.Version))
{
modelInfo.Version = ns.Version;
modelInfo.ModelVersion =
CoreUtils.FixupAsSemanticVersion(ns.Version);
}
if (!string.IsNullOrWhiteSpace(ns.PublicationDate))
{
modelInfo.PublicationDate = XmlConvert.ToDateTime(
ns.PublicationDate,
XmlDateTimeSerializationMode.Utc);
}
}
}
// save the dictionary in a member variable during processing.
m_dictionary = targetModel;
// build table of namespaces.
UpdateNamespaceTables(m_dictionary);
m_dictionary.TargetXmlNamespace = GetXmlNamespace(m_dictionary.TargetNamespace);
if (m_dictionary.Items == null || m_dictionary.Items.Length == 0)
{
m_logger.LogWarning("Nothing to do because design file has no entries.");
return;
}
UpdateNamespaceObject(m_dictionary);
}
///
/// Validate core model
///
private void ValidateCoreModel(
IReadOnlyList designFilePaths,
string identifierFilePath)
{
string inputPath = designFilePaths[0];
// initialize tables.
m_identifiers = [];
m_nodes = [];
m_namespaceTables = [];
m_nodesByNodeId = [];
m_browseNames = [];
m_startId = 15000;
m_designFilePaths = new Dictionary
{
[Ua.Types.Namespaces.OpcUa] = string.Empty
};
// load the design files.
m_logger.LogInformation("Loading StandardTypes...");
ModelDesign targetModel = LoadModelDesign(BuiltInDesignFiles.BuiltInTypesXml);
targetModel.Items ??= [];
for (int ii = 0; ii < designFilePaths.Count; ii++)
{
targetModel = LoadCoreDesignFile(targetModel, designFilePaths[ii]);
}
// set a default xml namespace.
if (string.IsNullOrEmpty(targetModel.TargetXmlNamespace))
{
targetModel.TargetXmlNamespace = targetModel.TargetNamespace;
if (!string.IsNullOrEmpty(ModelVersion))
{
targetModel.TargetVersion = ModelVersion;
}
else
{
ModelVersion = targetModel.TargetVersion;
}
if (!string.IsNullOrEmpty(ModelPublicationDate))
{
var dt = DateTime.Parse(
ModelPublicationDate,
CultureInfo.InvariantCulture,
DateTimeStyles.None);
targetModel.TargetPublicationDate = new DateTime(
dt.Year,
dt.Month,
dt.Day,
0,
0,
0,
DateTimeKind.Utc);
targetModel.TargetPublicationDateSpecified = true;
}
else
{
ModelPublicationDate = targetModel.TargetPublicationDateSpecified ?
targetModel.TargetPublicationDate.ToString(
"yyyy-MM-dd",
CultureInfo.InvariantCulture) :
null;
}
}
// mark the target namespace as found.
m_designFilePaths[targetModel.TargetNamespace] = inputPath;
// load any included design files.
if (targetModel.Namespaces != null)
{
m_dependencies = [];
for (int ii = 0; ii < targetModel.Namespaces.Length; ii++)
{
Namespace ns = targetModel.Namespaces[ii];
if (ns.Value != targetModel.TargetNamespace)
{
DateTime? pd = ns.PublicationDate != null ?
DateTime.Parse(
ns.PublicationDate,
CultureInfo.InvariantCulture,
DateTimeStyles.None) :
null;
m_dependencies[ns.Value] = new ModelTableEntry
{
ModelUri = ns.Value,
XmlSchemaUri = ns.XmlNamespace,
Version = ns.Version,
ModelVersion = CoreUtils.FixupAsSemanticVersion(ns.Version),
PublicationDate = pd ?? DateTime.MinValue,
PublicationDateSpecified = pd != null
};
}
// override the publication date.
if (ns.PublicationDate == null && ns.Version == null)
{
continue;
}
if (!m_dependencies.TryGetValue(ns.Value, out ModelTableEntry modelInfo))
{
continue;
}
if (!string.IsNullOrWhiteSpace(ns.Version))
{
modelInfo.Version = ns.Version;
modelInfo.ModelVersion = CoreUtils.FixupAsSemanticVersion(ns.Version);
}
if (!string.IsNullOrWhiteSpace(ns.PublicationDate))
{
modelInfo.PublicationDate = XmlConvert.ToDateTime(
ns.PublicationDate,
XmlDateTimeSerializationMode.Utc);
}
}
}
// save the model in a member variable during processing.
m_dictionary = targetModel;
// build table of namespaces.
UpdateNamespaceTables(m_dictionary);
m_dictionary.TargetXmlNamespace = GetXmlNamespace(m_dictionary.TargetNamespace);
// import types from target dictionary.
var nodes = new List();
foreach (NodeDesign node in m_dictionary.Items)
{
if (Import(m_dictionary, node, null))
{
nodes.Add(node);
}
}
if (nodes.Count == 0)
{
m_logger.LogWarning("Nothing to do because design files have no entries.");
return;
}
m_dictionary.Items = [.. nodes];
// do additional fix up after import.
ValidateDictionary(m_dictionary);
// validate node in target dictionary.
foreach (NodeDesign node in m_dictionary.Items)
{
node.Hierarchy = BuildInstanceHierarchy(node, 0);
}
AssignIdentifiers(m_dictionary, identifierFilePath);
UpdateNamespaceObject(m_dictionary);
}
///
/// Load built in model
///
///
private ModelDesign LoadBuiltInModel()
{
ModelDesign model = LoadBuiltInModels();
LoadNodes(model);
using Stream stream = OpenRead("StandardTypes.csv");
AssignIdentifiers(model, ParseIdentifiersFromStream(stream));
// flag built-in types as declarations.
foreach (NodeDesign node in model.Items)
{
node.Description = null;
node.IsDeclaration = true;
}
return model;
}
private void LoadNodes(ModelDesign model, bool validateDictionary = true)
{
UpdateNamespaceTables(model);
// import types from target dictionary.
var nodes = new List();
foreach (NodeDesign node in model.Items)
{
if (Import(model, node, null))
{
nodes.Add(node);
}
}
model.Items = [.. nodes];
// Validate node in target dictionary. Skipped when the model is
// being loaded only as a dependency (its nodes are registered in
// m_nodes so the primary target can resolve cross-model refs, but
// it was already validated when built as its own target, and
// re-validating here can fail if the dep imports types from
// another dep that has not yet been loaded).
if (validateDictionary)
{
ValidateDictionary(model);
}
// build hierarchy.
foreach (NodeDesign node in model.Items)
{
node.Hierarchy = BuildInstanceHierarchy(node, 0);
}
}
///
/// Load model design file
///
private ModelDesign LoadModelDesign(
string designFilePath,
string identifierFilePath,
bool validateDictionary = true)
{
ModelDesign model = LoadModelDesign(designFilePath);
model.SourceFilePath = designFilePath;
model.IsSourceNodeSet = false;
if (string.IsNullOrEmpty(model.TargetVersion))
{
model.TargetVersion = "1.0.0";
}
if (!model.TargetPublicationDateSpecified ||
model.TargetPublicationDate == DateTime.MinValue)
{
model.TargetPublicationDate = FileSystem.GetLastWriteTime(designFilePath);
model.TargetPublicationDateSpecified = true;
}
LoadNodes(model, validateDictionary);
// assigning identifiers.
identifierFilePath ??= Path.Combine(
Path.GetDirectoryName(designFilePath),
Path.GetFileNameWithoutExtension(designFilePath) + ".csv");
AssignIdentifiers(model, identifierFilePath);
return model;
}
private static LocalizedText ImportDocumentation(Documentation documentation)
{
if (documentation != null &&
documentation.Text != null &&
documentation.Text.Length > 0)
{
return new LocalizedText
{
Value = documentation.Text[0]?.Trim(),
IsAutogenerated = false
};
}
return null;
}
private static Parameter ImportField(FieldType field)
{
if (field == null)
{
return null;
}
var parameter = new Parameter
{
Name = field.Name
};
// parameter.Description = ImportDocumentation(field.Documentation);
if (field.DataType != null)
{
parameter.DataType = ImportTypeName(field.DataType);
}
else
{
parameter.DataType = s_baseDataTypeQn;
}
if (field.ValueRank is 0 or 1)
{
parameter.ValueRank = ValueRank.Array;
}
else if (field.ValueRank == 2)
{
parameter.ValueRank = ValueRank.Array;
parameter.ArrayDimensions = "0";
for (int ii = 1; ii < field.ValueRank; ii++)
{
parameter.ArrayDimensions = ",0";
}
}
else
{
parameter.ValueRank = ValueRank.Scalar;
}
parameter.IsOptional = field.IsOptional;
parameter.AllowSubTypes = field.AllowSubTypes;
parameter.DefaultValue = field.DefaultValue;
parameter.ReleaseStatus = (ReleaseStatus)(int)field.ReleaseStatus;
return parameter;
}
private static Parameter ImportEnumeratedValue(EnumeratedValue value)
{
if (value == null)
{
return null;
}
var parameter = new Parameter
{
Name = value.Name,
Description = ImportDocumentation(value.Documentation),
ReleaseStatus = (ReleaseStatus)(int)value.ReleaseStatus
};
if (value.ValueSpecified)
{
parameter.Identifier = value.Value;
parameter.IdentifierSpecified = true;
}
if (!string.IsNullOrEmpty(value.BitMask))
{
parameter.BitMask = value.BitMask;
parameter.IdentifierSpecified = false;
}
return parameter;
}
private static XmlQualifiedName ImportTypeName(XmlQualifiedName typeName)
{
if (typeName == null)
{
return null;
}
switch (typeName.Name)
{
case "ExtensionObject":
return s_structureQn;
case "Variant":
return s_baseDataTypeQn;
}
return new XmlQualifiedName(typeName.Name, Ua.Types.Namespaces.OpcUa);
}
private void ImportFields(DataTypeDesign design, FieldType[] fields)
{
if (fields != null && fields.Length > 0)
{
var parameters = new List();
for (int jj = 0; jj < fields.Length; jj++)
{
FieldType field = fields[jj];
Parameter parameter = ImportField(field);
if (!IsExcluded(parameter))
{
parameters.Add(parameter);
}
}
design.Fields = [.. parameters];
}
}
private ModelDesign ImportTypeDictionary(Stream stream)
{
var knownFiles = new Dictionary();
var validator = new TypeDictionaryValidator(FileSystem, knownFiles);
validator.Validate(stream);
string namespaceUri = validator.Dictionary.TargetNamespace;
if (namespaceUri == "http://opcfoundation.org/UA/Core/")
{
namespaceUri = Ua.Types.Namespaces.OpcUa;
}
var nodes = new List();
for (int ii = 0; ii < validator.Dictionary.Items.Length; ii++)
{
DataType dataType = validator.Dictionary.Items[ii];
if (IsExcluded(dataType))
{
continue;
}
var design = new DataTypeDesign
{
SymbolicId = new XmlQualifiedName(dataType.Name, namespaceUri)
};
design.SymbolicName = design.SymbolicId;
design.BaseType = s_baseDataTypeQn;
design.NoArraysAllowed = !dataType.AllowArrays;
design.NoClassGeneration = dataType.NotInAddressSpace;
design.NotInAddressSpace = dataType.NotInAddressSpace;
design.IsAbstract = false;
design.PartNo = dataType.PartNo;
design.Category = dataType.Category;
design.ReleaseStatus = (ReleaseStatus)(int)dataType.ReleaseStatus;
design.Purpose = (DataTypePurpose)(int)dataType.Purpose;
if (design.PartNo == 0)
{
design.PartNo = 4;
}
m_logger.LogDebug(
"Imported {Type}: {Name}",
design.GetType().Name,
design.SymbolicId.Name);
if (dataType is TypeDeclaration simpleType)
{
if (simpleType.SourceType != null)
{
design.BaseType = ImportTypeName(simpleType.SourceType);
}
nodes.Add(design);
}
if (dataType is ComplexType complexType)
{
if (complexType.BaseType != null)
{
design.BaseType = ImportTypeName(complexType.BaseType);
}
else
{
design.BaseType = s_structureQn;
}
ImportFields(design, complexType.Field);
design.IsAbstract = complexType.IsAbstract;
design.IsUnion = complexType.IsUnion;
nodes.Add(design);
}
if (dataType is ServiceType serviceType)
{
var service = new Service
{
Category = serviceType.InterfaceType switch
{
InterfaceType.Session => GetSessionSubCategory(dataType.Name),
InterfaceType.SecureChannel => ServiceCategory.SecureChannel,
InterfaceType.Discovery => ServiceCategory.Discovery,
InterfaceType.Registration => ServiceCategory.Registration,
InterfaceType.Test => ServiceCategory.Test,
_ => ServiceCategory.None
},
Name = dataType.Name
};
design.SymbolicId = new XmlQualifiedName(dataType.Name + "Request", namespaceUri);
design.SymbolicName = design.SymbolicId;
design.Service = service;
design.BaseType = s_structureQn;
design.BasicDataType = design.DetermineBasicDataType();
design.NoArraysAllowed = true;
design.NoClassGeneration = false;
design.NotInAddressSpace = true;
design.IsAbstract = false;
design.PartNo = 4;
design.Category = dataType.Category;
design.ReleaseStatus = (ReleaseStatus)(int)dataType.ReleaseStatus;
design.Purpose = DataTypePurpose.ServicesOnly;
ImportFields(design, serviceType.Request);
nodes.Add(design);
var design2 = new DataTypeDesign
{
SymbolicId = new XmlQualifiedName(dataType.Name + "Response", namespaceUri)
};
design2.SymbolicName = design2.SymbolicId;
design2.IsServiceResponse = true;
design2.Service = service;
design2.BaseType = s_structureQn;
design2.BasicDataType = design.DetermineBasicDataType();
design2.NoArraysAllowed = true;
design2.NoClassGeneration = false;
design2.NotInAddressSpace = true;
design2.IsAbstract = false;
design2.PartNo = 4;
design2.Category = dataType.Category;
design2.ReleaseStatus = (ReleaseStatus)(int)dataType.ReleaseStatus;
design2.Purpose = DataTypePurpose.ServicesOnly;
ImportFields(design2, serviceType.Response);
nodes.Add(design2);
service.Request = design;
service.Response = design2;
}
if (dataType is EnumeratedType enumeratedType)
{
design.BaseType = s_enumerationQn;
if (enumeratedType.IsOptionSet)
{
design.IsOptionSet = true;
if (enumeratedType.BaseType != null)
{
design.BaseType = ImportTypeName(enumeratedType.BaseType);
}
else
{
design.BaseType = new XmlQualifiedName("UInt32", Ua.Types.Namespaces.OpcUa);
}
}
if (enumeratedType.Value != null && enumeratedType.Value.Length > 0)
{
var parameters = new List();
for (int jj = 0; jj < enumeratedType.Value.Length; jj++)
{
EnumeratedValue value = enumeratedType.Value[jj];
Parameter parameter = ImportEnumeratedValue(value);
parameters.Add(parameter);
}
design.Fields = [.. parameters];
}
nodes.Add(design);
}
}
var model = new ModelDesign
{
Items = [.. nodes],
TargetNamespace = Ua.Types.Namespaces.OpcUa
};
model.TargetNamespace = validator.Dictionary.TargetVersion;
model.TargetPublicationDate = validator.Dictionary.TargetPublicationDate;
model.TargetPublicationDateSpecified = true;
return model;
}
private ModelDesign LoadBuiltInModels()
{
var nodes = new List();
// load the design files.
ModelDesign builtin = LoadModelDesign(
BuiltInDesignFiles.BuiltInTypesXml);
nodes.AddRange(builtin.Items);
ModelDesign datatypes = null;
using (Stream stream = OpenRead(
BuiltInDesignFiles.UACoreServicesXml))
{
datatypes = ImportTypeDictionary(stream);
}
if (datatypes != null)
{
nodes.AddRange(datatypes.Items);
}
ModelDesign standard = LoadModelDesign(
BuiltInDesignFiles.StandardTypesXml);
nodes.AddRange(standard.Items);
builtin.PermissionSets = standard.PermissionSets;
builtin.Items = [.. nodes];
return builtin;
}
private static RolePermissionSet ResolvePermissions(
ModelDesign dictionary,
RolePermissionSet input)
{
if (input?.Name != null)
{
var permissions = new Dictionary();
if (dictionary.PermissionSets != null)
{
RolePermissionSet template = dictionary.PermissionSets
.FirstOrDefault(x => x.Name == input.Name);
if (template?.RolePermission != null)
{
foreach (RolePermission jj in template.RolePermission)
{
permissions[jj.Role] = jj.Permission;
}
}
}
if (input?.RolePermission != null)
{
foreach (RolePermission jj in input?.RolePermission)
{
permissions[jj.Role] = jj.Permission;
}
}
if (permissions.Count > 0)
{
List combined = [];
foreach (KeyValuePair jj in permissions)
{
combined.Add(new RolePermission
{
Role = jj.Key,
Permission = jj.Value
});
}
return new RolePermissionSet
{
RolePermission = [.. combined],
DoNotInheirit = input?.DoNotInheirit ?? false
};
}
}
return null;
}
private void ResolvePermissions(ModelDesign dictionary, NodeDesign input)
{
input.RolePermissions = ResolvePermissions(
dictionary,
input.RolePermissions);
input.DefaultRolePermissions = ResolvePermissions(
dictionary,
input.DefaultRolePermissions);
if (input.DefaultRolePermissions != null)
{
m_defaultRolePermissions[input.SymbolicId.Name] =
input.DefaultRolePermissions;
}
if (input.DefaultAccessRestrictionsSpecified)
{
m_defaultAccessRestrictions[input.SymbolicId.Name] =
input.DefaultAccessRestrictions;
}
if (input.HasChildren)
{
foreach (NodeDesign child in input.Children.Items)
{
ResolvePermissions(dictionary, child);
}
}
}
private void ValidateDictionary(ModelDesign dictionary)
{
bool hasDataTypesDefined = false;
bool hasMethodsDefined = false;
foreach (NodeDesign node in dictionary.Items)
{
if (node is DataTypeDesign)
{
hasDataTypesDefined = true;
}
if (node is MethodDesign)
{
hasMethodsDefined = true;
}
Validate(node);
}
foreach (NodeDesign node in dictionary.Items)
{
ResolvePermissions(dictionary, node);
}
var nodes = new List();
nodes.AddRange(dictionary.Items);
if (hasDataTypesDefined)
{
AddDataTypeDictionary(
dictionary,
TargetNamespace,
EncodingType.Binary,
nodes);
AddDataTypeDictionary(
dictionary,
TargetNamespace,
EncodingType.Xml,
nodes);
foreach (NodeDesign node in dictionary.Items)
{
if (node is DataTypeDesign dataTypeDesign)
{
AddEnumStrings(dataTypeDesign);
}
}
}
dictionary.Items = [.. nodes];
// validate node in target dictionary.
if (hasMethodsDefined)
{
foreach (NodeDesign node in dictionary.Items)
{
if (node is MethodDesign methodDesign)
{
AddMethodArguments(methodDesign);
}
}
}
foreach (NodeDesign node in m_nodes.Values)
{
if (ReleaseCandidate &&
node.ReleaseStatus == ReleaseStatus.RC)
{
node.ReleaseStatus = ReleaseStatus.Released;
}
}
}
private void UpdateNamespaceObject(ModelDesign dictionary)
{
ObjectDesign metadata = null;
var dynamicIds = new HashSet();
foreach (NodeDesign node in dictionary.Items)
{
if (node.IsDynamic)
{
CollectDynamicIds(node, dynamicIds);
}
if (metadata == null)
{
metadata = node as ObjectDesign;
if (metadata != null &&
metadata.TypeDefinition != new XmlQualifiedName(
"NamespaceMetadataType",
Ua.Types.Namespaces.OpcUa))
{
metadata = null;
}
}
}
if (metadata == null)
{
return;
}
var ranges = new List();
int start = 1;
bool readingStaticRange = true;
for (uint ii = 1; dynamicIds.Count > 0; ii++)
{
if (readingStaticRange)
{
if (dynamicIds.Contains(ii))
{
readingStaticRange = false;
int end = (int)(ii - 1);
if (end > start)
{
ranges.Add(new NumericRange(start, end).ToString());
}
else
{
ranges.Add(new NumericRange(start).ToString());
}
dynamicIds.Remove(ii);
if (dynamicIds.Count == 0)
{
start = (int)ii + 1;
break;
}
}
}
else if (!dynamicIds.Remove(ii))
{
start = (int)ii;
readingStaticRange = true;
}
else if (dynamicIds.Count == 0)
{
start = (int)ii + 1;
break;
}
}
ranges.Add(new NumericRange(start, int.MaxValue).ToString());
// Fix avoid a null metadata in some cases
if (metadata.Hierarchy.NodeList != null)
{
foreach (HierarchyNode child in metadata.Hierarchy.NodeList)
{
if (child.Instance.BrowseName == "StaticNumericNodeIdRange")
{
var variable = child.Instance as VariableDesign;
variable.SetDefaultValue(ranges.ToArray(), m_context);
}
if (m_dictionary.TargetPublicationDateSpecified)
{
if (child.Instance.BrowseName == BrowseNames.NamespacePublicationDate)
{
var variable = child.Instance as VariableDesign;
variable.SetDefaultValue(m_dictionary.TargetPublicationDate, m_context);
}
}
if (!string.IsNullOrEmpty(m_dictionary.TargetVersion))
{
if (child.Instance.BrowseName == BrowseNames.NamespaceVersion)
{
var variable = child.Instance as VariableDesign;
variable.SetDefaultValue(m_dictionary.TargetVersion, m_context);
}
}
}
}
}
private static void CollectDynamicIds(NodeDesign node, HashSet dynamicIds)
{
dynamicIds.Add(node.NumericId);
if (node.Hierarchy.NodeList != null)
{
foreach (HierarchyNode child in node.Hierarchy.NodeList)
{
if (child.Instance.NumericIdSpecified)
{
dynamicIds.Add(child.Instance.NumericId);
}
}
}
}
private void UpdateNamespaceTables(ModelDesign dictionary)
{
// build table of namespaces.
Namespace targetNamespace = null;
NamespaceTable namespaceUris = dictionary.NamespaceUris ?? new NamespaceTable();
if (dictionary.Namespaces != null)
{
List namespaces = [.. dictionary.Namespaces];
for (int ii = 0; ii < namespaces.Count; ii++)
{
Namespace current = namespaces[ii];
current.Value = current.Value.Trim();
if (current.Value == dictionary.TargetNamespace)
{
targetNamespace = current;
}
if (current.Value != Ua.Types.Namespaces.OpcUa)
{
namespaceUris.GetIndexOrAppend(dictionary.Namespaces[ii].Value);
}
}
}
NamespaceUris = dictionary.NamespaceUris = namespaceUris;
TargetNamespace = targetNamespace;
}
private ModelDesign LoadCoreDesignFile(
ModelDesign dictionary,
string designFilePath)
{
ModelDesign model = null;
bool loadAsTypeDictionary = designFilePath == BuiltInDesignFiles.UACoreServicesXml;
if (!loadAsTypeDictionary)
{
m_logger.LogInformation("Loading DesignFile: {File}", designFilePath);
try
{
using Stream stream = OpenRead(designFilePath);
model = LoadInput(stream);
model.Items ??= [];
}
catch (Exception e)
{
m_logger.LogDebug(e,
"Error loading model {File}, trying as type dictionary",
designFilePath);
loadAsTypeDictionary = true;
}
}
if (loadAsTypeDictionary)
{
try
{
using Stream stream = OpenRead(designFilePath);
model = ImportTypeDictionary(stream);
}
catch (Exception e)
{
m_logger.LogError(e,
"Error parsing type dictionary {File}",
designFilePath);
throw new InvalidDataException("Error parsing file " + designFilePath, e);
}
}
if (dictionary != null)
{
var nodes2 = new List();
// namespaces in primary dictionary replace all namespaces in secondary dictionaries.
nodes2.AddRange(dictionary.Items);
nodes2.AddRange(model.Items);
if (model.PermissionSets?.Length > 0)
{
dictionary.PermissionSets = model.PermissionSets;
}
dictionary.Items = [.. nodes2];
model = dictionary;
}
foreach (NodeDesign node in model.Items)
{
if (node.Description == null)
{
continue;
}
if (!node.Description.DoNotIgnore)
{
node.Description = null;
}
}
return model;
}
private void ExcludeNodes(ModelDesign model)
{
var nodes = new List();
foreach (NodeDesign node in model.Items)
{
if (!IsExcluded(node))
{
nodes.Add(node);
}
}
model.Items = [.. nodes];
}
private void IndexNodesByNodeId(
NamespaceTable namespaceUris,
IEnumerable nodes,
IDictionary index,
NodeDesign parent)
{
foreach (NodeDesign node in nodes)
{
bool hasNumericId = node.NumericIdSpecified && node.NumericId > 0;
bool hasStringId = !node.NumericIdSpecified && !string.IsNullOrEmpty(node.StringId);
if (hasNumericId || hasStringId)
{
NodeId nodeId = hasNumericId ? new NodeId(node.NumericId) : new NodeId(node.StringId, 0);
nodeId = nodeId.WithNamespaceIndex(namespaceUris.GetIndexOrAppend(node.SymbolicId.Namespace));
index[nodeId] = node;
if (node.Children?.Items != null)
{
IndexNodesByNodeId(
namespaceUris,
node.Children.Items,
index,
parent ?? node);
}
}
else if (parent != null && parent.Hierarchy != null)
{
string id = node.SymbolicId.Name[(parent.SymbolicId.Name.Length + 1)..];
if (parent.Hierarchy.Nodes.TryGetValue(id, out HierarchyNode hierarchyNode))
{
var nodeId = new NodeId(
hierarchyNode.Instance.NumericId,
namespaceUris.GetIndexOrAppend(node.SymbolicId.Namespace));
index[nodeId] = node;
}
else
{
m_logger.LogDebug(
"Node {Node} missing from parent {Parent} hierarchy.",
node.SymbolicId,
parent.SymbolicId);
}
}
else
{
m_logger.LogDebug(
"Node {Node} does not have a valid NodeId.",
node);
}
}
}
private ModelDesign LoadDesignFile(
List namespaces,
string designFilePath,
string identifierFilePath,
bool validateDictionary = true)
{
m_logger.LogInformation("Loading DesignFile: {File}", designFilePath);
ModelDesign model;
string[] fields = designFilePath.Split(',');
string fileToLoad = fields[0];
string prefix = fields.Length > 1 ? fields[1] : null;
string name = fields.Length > 2 ? fields[2] : null;
if (NodeSetToModelDesign.IsNodeSet(FileSystem, fileToLoad))
{
var settings = new NodeSetReaderSettings
{
NodesByQName = m_nodes,
NamespaceTables = m_namespaceTables
};
foreach (Namespace ns in namespaces)
{
settings.NamespaceUris.GetIndexOrAppend(ns.Value);
}
IndexNodesByNodeId(
settings.NamespaceUris,
m_nodes.Values,
settings.NodesById,
null);
var reader = new NodeSetToModelDesign(
FileSystem,
fileToLoad,
settings,
m_telemetry);
model = reader.Import(prefix, name);
model.SourceFilePath = fileToLoad;
model.IsSourceNodeSet = true;
ExcludeNodes(model); // not consistent with model design, todo: consider removing
foreach (NodeDesign node in model.Items)
{
node.Hierarchy = BuildInstanceHierarchy(node, 0);
foreach (KeyValuePair ii in node.Hierarchy.Nodes)
{
if (ii.Value.Instance is InstanceDesign instance &&
instance.NumericId <= 0 &&
instance.StringId == null &&
instance.ModellingRule == ModellingRule.Mandatory)
{
// Not an error, show informational
m_logger.LogInformation(
"{Value} missing NodeId for Mandatory child in NodeSet.",
ii.Key);
}
}
}
}
else
{
model = LoadModelDesign(fileToLoad, identifierFilePath, validateDictionary);
Namespace ns = model.Namespaces
.FirstOrDefault(x => x.Value == model.TargetNamespace);
if (name != null)
{
ns.Name = name;
}
if (prefix != null)
{
ns.XmlPrefix = ns.Prefix = prefix;
}
}
return model;
}
private static void MergeNamespace(
List namespaces,
Namespace target)
{
for (int ii = 0; ii < namespaces.Count; ii++)
{
if (namespaces[ii].Value == target.Value)
{
string existingFilePath = namespaces[ii].FilePath;
if (existingFilePath == null)
{
namespaces[ii].FilePath = target.FilePath;
existingFilePath = target.FilePath;
}
if (string.CompareOrdinal(
namespaces[ii].PublicationDate,
target.PublicationDate) < 0)
{
namespaces[ii] = target;
// Preserve the authoritative FilePath: the previously
// merged entry (or the newly-arrived target) may have
// been the one actually produced from its own source
// file. Replacing with a dependency's import-declared
// namespace (FilePath == null) would otherwise lose
// the ability to subsequently load that file.
if (namespaces[ii].FilePath == null)
{
namespaces[ii].FilePath = existingFilePath;
}
}
return;
}
}
namespaces.Add(target);
}
private List GetNamespaceList(IReadOnlyList designFilePaths)
{
var namespaces = new List();
foreach (string path in designFilePaths)
{
string[] fields = path.Split(',');
string fileToLoad = fields[0];
string prefix = fields.Length > 1 ? fields[1] : null;
string name = fields.Length > 2 ? fields[2] : null;
List fileNamespaces = null;
if (NodeSetToModelDesign.IsNodeSet(FileSystem, fileToLoad))
{
fileNamespaces = NodeSetToModelDesign.LoadNamespaces(FileSystem, fileToLoad);
fileNamespaces[^1].FilePath = path;
}
else
{
ModelDesign design = LoadModelDesign(fileToLoad);
foreach (Namespace ns in design.Namespaces)
{
ns.Value = ns.Value.Trim();
ns.FilePath = ns.Value == design.TargetNamespace ? path : null;
}
fileNamespaces = [.. design.Namespaces
.Where(x => x.Name != "OpcUa" && x.Value != design.TargetNamespace)
.Reverse()];
fileNamespaces.Add(design.Namespaces
.FirstOrDefault(x => x.Value == design.TargetNamespace));
}
foreach (Namespace ns in fileNamespaces)
{
if (ns.Value != Ua.Types.Namespaces.OpcUa)
{
MergeNamespace(namespaces, ns);
}
}
Namespace target = fileNamespaces[^1];
foreach (Namespace ns in fileNamespaces)
{
if (ns.Value == target.Value)
{
break;
}
if (ns.Value == Ua.Types.Namespaces.OpcUa)
{
continue;
}
int index1 = FindNamespace(namespaces, target.Value);
int index2 = FindNamespace(namespaces, ns.Value);
if (index1 > index2)
{
namespaces.Insert(index2, namespaces[index1]);
namespaces.RemoveAt(index1 + 1);
}
}
}
return namespaces;
}
private static int FindNamespace(List namespaces, string uri)
{
for (int ii = 0; ii < namespaces.Count; ii++)
{
if (uri == namespaces[ii].Value)
{
return ii;
}
}
return -1;
}
private string GetXmlNamespace(string modelUri)
{
string ns = (
from x in m_dictionary.Namespaces
where x.Value == modelUri
select x.XmlNamespace).FirstOrDefault();
if (!string.IsNullOrEmpty(ns))
{
return ns;
}
return null;
}
private void AddMethodArguments(MethodDesign method)
{
var children = new List();
if (method.Children != null && method.Children.Items != null)
{
children.AddRange(method.Children.Items);
}
if (method.InputArguments != null && method.InputArguments.Length > 0)
{
var arguments = new List();
for (int ii = 0; ii < method.InputArguments.Length; ii++)
{
Parameter parameter = method.InputArguments[ii];
var argument = new Argument
{
Value = parameter,
Name = parameter.Name,
DataType = new NodeId(
parameter.DataType.ToString(),
(ushort)m_context.NamespaceUris.GetIndex(parameter.DataType.Namespace)),
ValueRank = ConstructValueRank(parameter.ValueRank, parameter.ArrayDimensions),
ArrayDimensions = ConstructArrayDimensionsRW(parameter.ValueRank, parameter.ArrayDimensions),
Description = default
};
if (!string.IsNullOrEmpty(parameter.ArrayDimensions))
{
var range = NumericRange.Parse(parameter.ArrayDimensions);
var dimensions = new List();
for (int jj = 0; jj < range.Dimensions; jj++)
{
dimensions.Add((uint)range.SubRanges[jj].Begin);
}
argument.ArrayDimensions = dimensions;
}
if (parameter.Description != null && !parameter.Description.IsAutogenerated)
{
argument.Description = new Ua.LocalizedText(
parameter.Description.Key,
string.Empty,
parameter.Description.Value?.Trim());
}
arguments.Add(argument);
}
AddProperty(
method,
new XmlQualifiedName("InputArguments", Ua.Types.Namespaces.OpcUa),
new XmlQualifiedName("Argument", Ua.Types.Namespaces.OpcUa),
ValueRank.Array,
[(uint)arguments.Count],
arguments.ToArray(),
children);
}
if (method.OutputArguments != null && method.OutputArguments.Length > 0)
{
var arguments = new List();
for (int ii = 0; ii < method.OutputArguments.Length; ii++)
{
Parameter parameter = method.OutputArguments[ii];
var argument = new Argument
{
Value = parameter,
Name = parameter.Name,
DataType = new NodeId(parameter.DataType.ToString(),
(ushort)m_context.NamespaceUris.GetIndex(parameter.DataType.Namespace)),
ValueRank = ConstructValueRank(parameter.ValueRank, parameter.ArrayDimensions),
ArrayDimensions = ConstructArrayDimensionsRW(parameter.ValueRank, parameter.ArrayDimensions),
Description = default
};
if (parameter.Description != null && !parameter.Description.IsAutogenerated)
{
argument.Description = new Ua.LocalizedText(
parameter.Description.Key,
string.Empty,
parameter.Description.Value?.Trim());
}
arguments.Add(argument);
}
AddProperty(
method,
new XmlQualifiedName("OutputArguments", Ua.Types.Namespaces.OpcUa),
new XmlQualifiedName("Argument", Ua.Types.Namespaces.OpcUa),
ValueRank.Array,
[(uint)arguments.Count],
arguments.ToArray(),
children);
}
method.Children = new ListOfChildren
{
Items = [.. children]
};
}
private void AddEnumStrings(DataTypeDesign dataType)
{
var children = new List();
if (dataType.Children != null && dataType.Children.Items != null)
{
children.AddRange(dataType.Children.Items);
}
if (!dataType.IsEnumeration ||
dataType.Fields == null ||
dataType.Fields.Length == 0)
{
return;
}
if (dataType.IsOptionSet)
{
var values = new List();
int last = 0;
for (int ii = 0; ii < 32; ii++)
{
int hit = 1 << ii;
foreach (Parameter parameter in dataType.Fields)
{
if (parameter.Identifier == hit)
{
while (last++ < ii)
{
values.Add(new Ua.LocalizedText(string.Empty, "Reserved"));
}
if (parameter.DisplayName?.Value != null && !parameter.DisplayName.IsAutogenerated)
{
values.Add(new Ua.LocalizedText(string.Empty, parameter.DisplayName.Value));
}
else
{
values.Add(new Ua.LocalizedText(string.Empty, parameter.Name));
}
last = ii + 1;
break;
}
}
}
AddProperty(
dataType,
new XmlQualifiedName("OptionSetValues", Ua.Types.Namespaces.OpcUa),
new XmlQualifiedName("LocalizedText", Ua.Types.Namespaces.OpcUa),
ValueRank.Array,
[(uint)values.Count],
values.ToArray(),
children);
}
else
{
int index = 0;
bool sequentenial = false;
if (!dataType.ForceEnumValues)
{
sequentenial = true;
foreach (Parameter parameter in dataType.Fields)
{
if (parameter.Identifier != index)
{
sequentenial = false;
break;
}
index++;
}
}
if (sequentenial)
{
var values = new List();
foreach (Parameter parameter in dataType.Fields)
{
if (parameter.DisplayName?.Value != null && !parameter.DisplayName.IsAutogenerated)
{
values.Add(new Ua.LocalizedText(string.Empty, parameter.DisplayName.Value));
}
else
{
values.Add(new Ua.LocalizedText(string.Empty, parameter.Name));
}
}
AddProperty(
dataType,
new XmlQualifiedName("EnumStrings", Ua.Types.Namespaces.OpcUa),
new XmlQualifiedName("LocalizedText", Ua.Types.Namespaces.OpcUa),
ValueRank.Array,
[(uint)values.Count],
values.ToArray(),
children);
}
else
{
var values = new List();
foreach (Parameter parameter in dataType.Fields)
{
var value = new EnumValueType();
if (parameter.DisplayName?.Value != null && !parameter.DisplayName.IsAutogenerated)
{
value.DisplayName = new Ua.LocalizedText(string.Empty, parameter.DisplayName.Value);
}
else
{
value.DisplayName = new Ua.LocalizedText(string.Empty, parameter.Name);
}
value.Value = (long)parameter.Identifier;
if (parameter.Description != null && !parameter.Description.IsAutogenerated)
{
value.Description = new Ua.LocalizedText(parameter.Description.Key, string.Empty, parameter.Description.Value?.Trim());
}
values.Add(value);
}
AddProperty(
dataType,
new XmlQualifiedName("EnumValues", Ua.Types.Namespaces.OpcUa),
new XmlQualifiedName("EnumValueType", Ua.Types.Namespaces.OpcUa),
ValueRank.Array,
[(uint)values.Count],
values.ToArray(),
children);
}
}
dataType.Children = new ListOfChildren
{
Items = [.. children]
};
}
private void AddDataTypeDictionary(
ModelDesign nodes,
Namespace ns,
EncodingType encodingType,
List nodesToAdd)
{
var descriptions = new List();
DictionaryDesign dictionary;
{
dictionary = new DictionaryDesign();
string namespaceUri = ns.Value;
bool isXml = encodingType == EncodingType.Xml;
if (isXml && !string.IsNullOrEmpty(ns.XmlNamespace))
{
namespaceUri = ns.XmlNamespace;
}
if (!isXml)
{
dictionary.SymbolicId = new XmlQualifiedName(
NodeDesign.CreateSymbolicId(ns.Name, "BinarySchema"),
ns.Value);
}
else
{
dictionary.SymbolicId = new XmlQualifiedName(
NodeDesign.CreateSymbolicId(ns.Name, "XmlSchema"),
ns.Value);
}
dictionary.SymbolicName = dictionary.SymbolicId;
dictionary.BrowseName = ns.Prefix;
dictionary.DisplayName = new LocalizedText
{
IsAutogenerated = true,
Value = dictionary.BrowseName
};
dictionary.WriteAccess = 0;
dictionary.TypeDefinition = new XmlQualifiedName(
"DataTypeDictionaryType",
Ua.Types.Namespaces.OpcUa);
dictionary.TypeDefinitionNode = this.FindNode(
dictionary.TypeDefinition,
dictionary.SymbolicId.Name,
"TypeDefinition");
dictionary.DataType = s_byteStringQn;
dictionary.DataTypeNode = this.FindNode(
dictionary.DataType,
dictionary.SymbolicId.Name,
"DataType");
dictionary.ValueRank = ValueRank.Scalar;
dictionary.ValueRankSpecified = true;
dictionary.ArrayDimensions = null;
dictionary.AccessLevel = AccessLevel.Read;
dictionary.AccessLevelSpecified = true;
dictionary.MinimumSamplingInterval = 0;
dictionary.MinimumSamplingIntervalSpecified = true;
dictionary.Historizing = false;
dictionary.HistorizingSpecified = true;
if (m_standardVersion != SpecificationVersion.V103)
{
dictionary.ReleaseStatus = ReleaseStatus.Deprecated;
}
if (ns.Value == Ua.Types.Namespaces.OpcUa)
{
dictionary.PartNo = 5;
}
var reference = new Reference
{
ReferenceType = new XmlQualifiedName(
"HasComponent",
Ua.Types.Namespaces.OpcUa),
IsInverse = true,
IsOneWay = false
};
if (isXml)
{
reference.TargetId = new XmlQualifiedName(
"XmlSchema_TypeSystem",
Ua.Types.Namespaces.OpcUa);
}
else
{
reference.TargetId = new XmlQualifiedName(
"OPCBinarySchema_TypeSystem",
Ua.Types.Namespaces.OpcUa);
}
dictionary.References = [reference];
AddProperty(
dictionary,
new XmlQualifiedName("NamespaceUri", Ua.Types.Namespaces.OpcUa),
new XmlQualifiedName("String", Ua.Types.Namespaces.OpcUa),
ValueRank.Scalar,
null,
namespaceUri,
descriptions);
if (m_standardVersion != SpecificationVersion.V103)
{
AddProperty(
dictionary,
new XmlQualifiedName("Deprecated", Ua.Types.Namespaces.OpcUa),
new XmlQualifiedName("Boolean", Ua.Types.Namespaces.OpcUa),
ValueRank.Scalar,
null,
true,
descriptions);
}
}
foreach (NodeDesign node in nodes.Items)
{
if (node is DataTypeDesign dataType &&
dataType.BasicDataType == BasicDataType.UserDefined)
{
AddDataTypeDescription(dataType, dictionary, descriptions, encodingType, nodesToAdd);
}
}
dictionary.Children = new ListOfChildren
{
Items = [.. descriptions]
};
m_nodes[dictionary.SymbolicId] = dictionary;
m_logger.LogDebug(
"Added {Type}: {Name}",
dictionary.GetType().Name,
dictionary.SymbolicId.Name);
nodesToAdd.Add(dictionary);
}
private void AddProperty(
NodeDesign parent,
XmlQualifiedName propertyName,
XmlQualifiedName dataType,
ValueRank valueRank,
uint[] arrayDimensions,
object value,
List children)
{
var property = new PropertyDesign
{
Parent = parent,
ReferenceType = new XmlQualifiedName("HasProperty", Ua.Types.Namespaces.OpcUa),
ModellingRule = ModellingRule.Mandatory,
ModellingRuleSpecified = true,
SymbolicId = new XmlQualifiedName(
NodeDesign.CreateSymbolicId(parent.SymbolicId.Name, propertyName.Name),
parent.SymbolicId.Namespace),
SymbolicName = propertyName,
BrowseName = propertyName.Name
};
property.DisplayName = new LocalizedText
{
IsAutogenerated = true,
Value = property.BrowseName
};
property.WriteAccess = 0;
property.TypeDefinition = new XmlQualifiedName(
"PropertyType",
Ua.Types.Namespaces.OpcUa);
property.TypeDefinitionNode = this.FindNode(
property.TypeDefinition,
property.SymbolicId.Name,
"TypeDefinition");
property.DataType = dataType;
property.DataTypeNode = this.FindNode(
property.DataType,
property.SymbolicId.Name,
"DataType");
property.ValueRank = valueRank;
property.ValueRankSpecified = true;
property.ArrayDimensions = null;
property.AccessLevel = AccessLevel.Read;
property.AccessLevelSpecified = true;
property.MinimumSamplingInterval = 0;
property.MinimumSamplingIntervalSpecified = true;
property.Historizing = false;
property.HistorizingSpecified = true;
property.SetDefaultValue(value, m_context);
property.PartNo = parent.PartNo;
property.Category = parent.Category;
property.ReleaseStatus = parent.ReleaseStatus;
if (m_standardVersion != SpecificationVersion.V104 &&
m_standardVersion != SpecificationVersion.V103 &&
arrayDimensions != null)
{
property.ArrayDimensions = string.Empty;
foreach (uint ii in arrayDimensions)
{
if (property.ArrayDimensions.Length > 0)
{
property.ArrayDimensions += ",";
}
property.ArrayDimensions +=
ii.ToString(CultureInfo.InvariantCulture);
}
}
children.Add(property);
m_nodes[property.SymbolicId] = property;
m_logger.LogDebug(
"Added {Type}: {Name}",
property.GetType().Name,
property.SymbolicId.Name);
}
private void AddDataTypeDescription(
DataTypeDesign dataType,
DictionaryDesign dictionary,
List descriptions,
EncodingType encodingType,
IList nodesToAdd)
{
VariableDesign description = null;
if (!dataType.NotInAddressSpace)
{
description = new VariableDesign
{
Parent = dictionary,
ReferenceType = new XmlQualifiedName(
"HasComponent",
Ua.Types.Namespaces.OpcUa),
ModellingRule = ModellingRule.Mandatory,
ModellingRuleSpecified = true,
SymbolicId = new XmlQualifiedName(
NodeDesign.CreateSymbolicId(
dictionary.SymbolicId.Name,
dataType.SymbolicId.Name),
dictionary.SymbolicId.Namespace)
};
description.SymbolicName = new XmlQualifiedName(
dataType.SymbolicId.Name,
description.SymbolicId.Namespace);
description.BrowseName = dataType.BrowseName;
description.DisplayName = new LocalizedText
{
IsAutogenerated = true,
Value = description.BrowseName
};
description.WriteAccess = 0;
description.TypeDefinition = new XmlQualifiedName(
"DataTypeDescriptionType",
Ua.Types.Namespaces.OpcUa);
description.TypeDefinitionNode = this.FindNode(
description.TypeDefinition,
description.SymbolicId.Name,
"TypeDefinition");
description.DataType = new XmlQualifiedName(
"String",
Ua.Types.Namespaces.OpcUa);
description.DataTypeNode = this.FindNode(
description.DataType,
description.SymbolicId.Name,
"DataType");
description.ValueRank = ValueRank.Scalar;
description.ValueRankSpecified = true;
description.ArrayDimensions = null;
description.AccessLevel = AccessLevel.Read;
description.AccessLevelSpecified = true;
description.MinimumSamplingInterval = 0;
description.MinimumSamplingIntervalSpecified = true;
description.Historizing = false;
description.HistorizingSpecified = true;
description.PartNo = dataType.PartNo;
description.NotInAddressSpace = dataType.NotInAddressSpace;
description.ReleaseStatus = dataType.ReleaseStatus;
description.Purpose = dataType.Purpose;
if (encodingType == EncodingType.Xml)
{
description.DecodedValue =
CoreUtils.Format("//xs:element[@name='{0}']", dataType.SymbolicName.Name);
}
else
{
description.DecodedValue =
CoreUtils.Format("{0}", dataType.SymbolicName.Name);
}
if (description.ReleaseStatus == ReleaseStatus.Released &&
m_standardVersion != SpecificationVersion.V103)
{
description.ReleaseStatus = ReleaseStatus.Deprecated;
}
if (!dataType.NotInAddressSpace)
{
descriptions.Add(description);
}
m_nodes[description.SymbolicId] = description;
m_logger.LogDebug(
"Added {Type}: {Name}",
description.GetType().Name,
description.SymbolicId.Name);
}
if (dataType.BasicDataType == BasicDataType.UserDefined && !dataType.NoEncodings)
{
AddDataTypeEncoding(dataType, description, encodingType, nodesToAdd);
}
}
private enum EncodingType
{
Binary = 0,
Xml = 1
}
private void AddDataTypeEncoding(
DataTypeDesign dataType,
VariableDesign description,
EncodingType encodingType,
IList nodesToAdd)
{
var encoding = new ObjectDesign
{
Parent = null,
ReferenceType = null
};
if (encodingType == EncodingType.Xml)
{
encoding.SymbolicId = new XmlQualifiedName(
dataType.SymbolicId.Name + "_Encoding_DefaultXml",
dataType.SymbolicId.Namespace);
encoding.SymbolicName = new XmlQualifiedName("DefaultXml", Ua.Types.Namespaces.OpcUa);
encoding.BrowseName = "Default XML";
}
else
{
encoding.SymbolicId = new XmlQualifiedName(
dataType.SymbolicId.Name + "_Encoding_DefaultBinary",
dataType.SymbolicId.Namespace);
encoding.SymbolicName = new XmlQualifiedName("DefaultBinary", Ua.Types.Namespaces.OpcUa);
encoding.BrowseName = "Default Binary";
}
encoding.DisplayName = new LocalizedText
{
IsAutogenerated = true,
Value = encoding.BrowseName
};
encoding.WriteAccess = 0;
encoding.TypeDefinition = new XmlQualifiedName(
"DataTypeEncodingType",
Ua.Types.Namespaces.OpcUa);
encoding.TypeDefinitionNode = this.FindNode(
encoding.TypeDefinition,
encoding.SymbolicId.Name,
"TypeDefinition");
encoding.SupportsEvents = false;
encoding.SupportsEventsSpecified = true;
encoding.PartNo = dataType.PartNo;
encoding.NotInAddressSpace = dataType.NotInAddressSpace;
encoding.Category = dataType.Category;
encoding.ReleaseStatus = dataType.ReleaseStatus;
encoding.Purpose = dataType.Purpose;
encoding.Parent = dataType;
var reference1 = new Reference
{
ReferenceType = new XmlQualifiedName("HasEncoding", Ua.Types.Namespaces.OpcUa),
IsInverse = true,
IsOneWay = false,
TargetId = dataType.SymbolicId,
TargetNode = dataType
};
if (description != null && !dataType.NotInAddressSpace)
{
var reference2 = new Reference
{
ReferenceType = new XmlQualifiedName("HasDescription", Ua.Types.Namespaces.OpcUa),
IsInverse = false,
IsOneWay = false,
TargetId = description.SymbolicId,
TargetNode = description
};
encoding.References = [reference1, reference2];
}
else
{
encoding.References = [reference1];
}
m_nodes[encoding.SymbolicId] = encoding;
nodesToAdd.Add(encoding);
m_logger.LogDebug(
"Added {Type}: {Name}",
encoding.GetType().Name,
encoding.SymbolicId.Name);
}
///
/// Returns true if the node is a declaration.
///
private static bool IsDeclaration(NodeDesign node)
{
for (NodeDesign parent = node; parent != null; parent = parent.Parent)
{
if (parent.IsDeclaration)
{
return true;
}
}
return false;
}
private class IdInfo
{
public object Id;
public string SymbolicId;
public NodeClass NodeClass;
public ReleaseStatus ReleaseStatus;
public string Category;
}
private Dictionary ParseIdentifiersFromStream(Stream istrm)
{
var identifiers = new Dictionary();
var reader = new StreamReader(istrm);
try
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
if (string.IsNullOrEmpty(line) || line.StartsWith('#'))
{
continue;
}
int index = line.IndexOf(',', StringComparison.Ordinal);
if (index == -1)
{
continue;
}
// remove the node class if it is present.
int lastIndex = line.LastIndexOf(',');
if (lastIndex != -1 && index != lastIndex)
{
line = line[..lastIndex];
}
try
{
string name = line[..index].Trim();
string id = line[(index + 1)..].Trim();
m_logger.LogDebug("Loaded ID: {Name}={Id}", name, id);
if (id.StartsWith('"'))
{
identifiers[name] = id[1..^1];
}
else
{
identifiers[name] = Convert.ToUInt32(id, CultureInfo.InvariantCulture);
}
}
catch (Exception ex)
{
m_logger.LogWarning(ex, "Error loading ID");
}
}
}
finally
{
reader.Close();
}
return identifiers;
}
private object AssignIdToNode(
NodeDesign node,
Dictionary identifiers,
SortedDictionary