mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 16:52:07 +08:00
6476 lines
233 KiB
C#
6476 lines
233 KiB
C#
/* ========================================================================
|
|
* 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.SourceGeneration.Dependency;
|
|
using Opc.Ua.Types;
|
|
|
|
namespace Opc.Ua.Schema.Model
|
|
{
|
|
/// <summary>
|
|
/// Generates files used to describe data types.
|
|
/// </summary>
|
|
public class ModelDesignValidator : SchemaValidator, IModelDesign
|
|
{
|
|
/// <summary>
|
|
/// Create model design validator
|
|
/// </summary>
|
|
public ModelDesignValidator(
|
|
IFileSystem fileSystem,
|
|
uint startId,
|
|
IReadOnlyList<string> exclusions,
|
|
ITelemetryContext telemetry,
|
|
SpecificationVersion standardVersion)
|
|
: base(fileSystem, null, null)
|
|
{
|
|
m_telemetry = telemetry;
|
|
m_logger = telemetry.CreateLogger<ModelDesignValidator>();
|
|
m_context = ServiceMessageContext.CreateEmpty(telemetry);
|
|
m_startId = startId;
|
|
m_exclusions = exclusions;
|
|
m_standardVersion = standardVersion;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IEnumerable<ModelTableEntry> Dependencies => m_dependencies.Values;
|
|
|
|
/// <inheritdoc/>
|
|
public Namespace[] Namespaces => m_dictionary?.Namespaces;
|
|
|
|
/// <inheritdoc/>
|
|
public Namespace TargetNamespace { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public DateTime? TargetPublicationDate =>
|
|
m_dictionary.TargetPublicationDateSpecified &&
|
|
m_dictionary.TargetPublicationDate != DateTime.MinValue ?
|
|
m_dictionary.TargetPublicationDate : null;
|
|
|
|
/// <inheritdoc/>
|
|
public string TargetVersion => m_dictionary.TargetVersion;
|
|
|
|
/// <inheritdoc/>
|
|
public bool UseAllowSubtypes { get; set; }
|
|
|
|
/// <inheritdoc/>
|
|
public NamespaceTable NamespaceUris { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public NodeDesign[] Nodes => m_dictionary.Items;
|
|
|
|
/// <inheritdoc/>
|
|
public IReadOnlyDictionary<string, RolePermissionSet> RolePermissions
|
|
=> m_defaultRolePermissions;
|
|
|
|
/// <inheritdoc/>
|
|
public IReadOnlyDictionary<string, AccessRestrictions?> AccessRestrictions
|
|
=> m_defaultAccessRestrictions;
|
|
|
|
/// <summary>
|
|
/// Max recursion
|
|
/// </summary>
|
|
public int MaxRecursionDepth { get; set; } = 100;
|
|
|
|
/// <summary>
|
|
/// Is Release candidate
|
|
/// </summary>
|
|
public bool ReleaseCandidate { get; set; }
|
|
|
|
/// <summary>
|
|
/// ModelVersion
|
|
/// </summary>
|
|
public string ModelVersion { get; set; }
|
|
|
|
/// <summary>
|
|
/// ModelPublicationDate
|
|
/// </summary>
|
|
public string ModelPublicationDate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Returns a list of services filter by their service category.
|
|
/// </summary>
|
|
public Service[] GetListOfServices(params ServiceCategory[] serviceCategories)
|
|
{
|
|
return [.. m_nodes.Values
|
|
.OfType<DataTypeDesign>()
|
|
.Where(x =>
|
|
!IsExcluded(x) &&
|
|
x.Service != null &&
|
|
(
|
|
serviceCategories == null ||
|
|
serviceCategories.Length == 0 ||
|
|
serviceCategories.Contains(x.Service.Category)
|
|
))
|
|
.Select(x => x.Service)
|
|
.Distinct()];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of nodes in the design that should be
|
|
/// used to generate code for
|
|
/// </summary>
|
|
public IEnumerable<NodeDesign> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the specified target design files and identifier file, using
|
|
/// the supplied <paramref name="dependencyFilePaths"/> solely for cross-model
|
|
/// reference resolution (dependencies are never code-generated).
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
/// <param name="targetFilePaths">Design files to generate code for.
|
|
/// Must contain at least one entry; the first entry is the primary
|
|
/// model paired with <paramref name="identifierFilePath"/>.</param>
|
|
/// <param name="dependencyFilePaths">Design files providing namespace
|
|
/// resolution only (e.g. companion specs referenced by the targets).
|
|
/// May be empty or <c>null</c>.</param>
|
|
/// <param name="identifierFilePath">Optional CSV identifier file
|
|
/// associated with the primary target.</param>
|
|
public void Validate(
|
|
IReadOnlyList<string> targetFilePaths,
|
|
IReadOnlyList<string> 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<string>(
|
|
targetFilePaths.Count + dependencyFilePaths.Count);
|
|
allFilePaths.AddRange(targetFilePaths);
|
|
allFilePaths.AddRange(dependencyFilePaths);
|
|
|
|
ValidateModel(allFilePaths, identifierFilePath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is excluded design
|
|
/// </summary>
|
|
/// <param name="node"></param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is excluded data type
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is excluded field
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find node design
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate model
|
|
/// </summary>
|
|
/// <param name="designFilePaths"></param>
|
|
/// <param name="identifierFilePath"></param>
|
|
private void ValidateModel(
|
|
List<string> 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;
|
|
|
|
// Apply any dependency payloads that were registered via ImportDependency()
|
|
// before validation began. Their type entries become visible
|
|
// to the dependency-loading pass below, so downstream
|
|
// NodeSet2/ModelDesign inputs can resolve cross-namespace
|
|
// references (e.g. BaseType lookups) without those upstream
|
|
// models being present in AdditionalFiles.
|
|
ApplyPendingDependencies();
|
|
|
|
// load the design files.
|
|
List<Namespace> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate core model
|
|
/// </summary>
|
|
private void ValidateCoreModel(
|
|
IReadOnlyList<string> 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<string, string>
|
|
{
|
|
[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<NodeDesign>();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load built in model
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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<NodeDesign>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load model design file
|
|
/// </summary>
|
|
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<Parameter>();
|
|
|
|
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<string, string>();
|
|
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<NodeDesign>();
|
|
|
|
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;
|
|
}
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
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<Parameter>();
|
|
|
|
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<NodeDesign>();
|
|
|
|
// 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<XmlQualifiedName, Permissions[]>();
|
|
|
|
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<RolePermission> combined = [];
|
|
|
|
foreach (KeyValuePair<XmlQualifiedName, Permissions[]> 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<NodeDesign>();
|
|
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<uint>();
|
|
|
|
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<string>();
|
|
|
|
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<uint> 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<Namespace> 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)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.LogInformation("Loading DesignFile: {File}", designFilePath);
|
|
}
|
|
try
|
|
{
|
|
using Stream stream = OpenRead(designFilePath);
|
|
model = LoadInput<ModelDesign>(stream);
|
|
model.Items ??= [];
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
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<NodeDesign>();
|
|
|
|
// 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<NodeDesign>();
|
|
|
|
foreach (NodeDesign node in model.Items)
|
|
{
|
|
if (!IsExcluded(node))
|
|
{
|
|
nodes.Add(node);
|
|
}
|
|
}
|
|
|
|
model.Items = [.. nodes];
|
|
}
|
|
|
|
private void IndexNodesByNodeId(
|
|
NamespaceTable namespaceUris,
|
|
IEnumerable<NodeDesign> nodes,
|
|
IDictionary<NodeId, NodeDesign> 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 if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Node {Node} missing from parent {Parent} hierarchy.",
|
|
node.SymbolicId,
|
|
parent.SymbolicId);
|
|
}
|
|
}
|
|
else if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Node {Node} does not have a valid NodeId.",
|
|
node);
|
|
}
|
|
}
|
|
}
|
|
|
|
private ModelDesign LoadDesignFile(
|
|
List<Namespace> namespaces,
|
|
string designFilePath,
|
|
string identifierFilePath,
|
|
bool validateDictionary = true)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
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<string, HierarchyNode> 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
|
|
if (m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
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<Namespace> 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<Namespace> GetNamespaceList(IReadOnlyList<string> designFilePaths)
|
|
{
|
|
var namespaces = new List<Namespace>();
|
|
|
|
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<Namespace> 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<Namespace> 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<InstanceDesign>();
|
|
|
|
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<Argument>();
|
|
|
|
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<uint>();
|
|
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<Argument>();
|
|
|
|
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<InstanceDesign>();
|
|
|
|
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<Ua.LocalizedText>();
|
|
|
|
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<Ua.LocalizedText>();
|
|
|
|
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<EnumValueType>();
|
|
|
|
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<NodeDesign> nodesToAdd)
|
|
{
|
|
var descriptions = new List<InstanceDesign>();
|
|
|
|
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<VariableTypeDesign>(
|
|
dictionary.TypeDefinition,
|
|
dictionary.SymbolicId.Name,
|
|
"TypeDefinition");
|
|
dictionary.DataType = s_byteStringQn;
|
|
dictionary.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
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;
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
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<InstanceDesign> 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<VariableTypeDesign>(
|
|
property.TypeDefinition,
|
|
property.SymbolicId.Name,
|
|
"TypeDefinition");
|
|
property.DataType = dataType;
|
|
property.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
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;
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Added {Type}: {Name}",
|
|
property.GetType().Name,
|
|
property.SymbolicId.Name);
|
|
}
|
|
}
|
|
|
|
private void AddDataTypeDescription(
|
|
DataTypeDesign dataType,
|
|
DictionaryDesign dictionary,
|
|
List<InstanceDesign> descriptions,
|
|
EncodingType encodingType,
|
|
IList<NodeDesign> 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<VariableTypeDesign>(
|
|
description.TypeDefinition,
|
|
description.SymbolicId.Name,
|
|
"TypeDefinition");
|
|
description.DataType = new XmlQualifiedName(
|
|
"String",
|
|
Ua.Types.Namespaces.OpcUa);
|
|
description.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
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;
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
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<NodeDesign> 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<ObjectTypeDesign>(
|
|
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);
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Added {Type}: {Name}",
|
|
encoding.GetType().Name,
|
|
encoding.SymbolicId.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the node is a declaration.
|
|
/// </summary>
|
|
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<string, object> ParseIdentifiersFromStream(Stream istrm)
|
|
{
|
|
var identifiers = new Dictionary<string, object>();
|
|
|
|
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();
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
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<string, object> identifiers,
|
|
SortedDictionary<object, IdInfo> uniqueIdentifiers,
|
|
Dictionary<string, object> duplicateIdentifiers,
|
|
IdAllocator assignedIds,
|
|
bool isImplicitlyDefined)
|
|
{
|
|
// assign identifier if one has not already been assigned.
|
|
|
|
if (!identifiers.TryGetValue(node.SymbolicId.Name, out object id))
|
|
{
|
|
if (m_symbolicIdToNodeId.TryGetValue(node.SymbolicId, out NodeId nodeId))
|
|
{
|
|
if (nodeId.TryGetValue(out uint numeric))
|
|
{
|
|
id = numeric;
|
|
}
|
|
else if (nodeId.TryGetValue(out string stringId))
|
|
{
|
|
id = stringId;
|
|
}
|
|
else
|
|
{
|
|
id = assignedIds.FindUnusedId(node.SymbolicId.Namespace, isImplicitlyDefined);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
id = assignedIds.FindUnusedId(node.SymbolicId.Namespace, isImplicitlyDefined);
|
|
}
|
|
|
|
identifiers.Add(node.SymbolicId.Name, id);
|
|
}
|
|
else if (isImplicitlyDefined)
|
|
{
|
|
id = assignedIds.FindUnusedId(node.SymbolicId.Namespace, isImplicitlyDefined);
|
|
identifiers[node.SymbolicId.Name] = id;
|
|
}
|
|
|
|
// save identifier.
|
|
if (uniqueIdentifiers.ContainsKey(id))
|
|
{
|
|
duplicateIdentifiers.Add(node.SymbolicId.Name, id);
|
|
}
|
|
else
|
|
{
|
|
var info = new IdInfo
|
|
{
|
|
Id = id,
|
|
SymbolicId = node.SymbolicId.Name,
|
|
NodeClass = GetNodeClass(node),
|
|
ReleaseStatus = node.ReleaseStatus,
|
|
Category = node.Category
|
|
};
|
|
|
|
uniqueIdentifiers.Add(id, info);
|
|
}
|
|
|
|
// set identifier for node.
|
|
if (id is uint numericId)
|
|
{
|
|
node.NumericId = numericId;
|
|
node.NumericIdSpecified = true;
|
|
node.StringId = null;
|
|
}
|
|
else
|
|
{
|
|
node.NumericId = 0;
|
|
node.NumericIdSpecified = false;
|
|
node.StringId = id as string;
|
|
}
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug("Assigned ID: {Name}={Id}", node.SymbolicId.Name, id);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
private static bool IsExplicitlyDefined(HierarchyNode current, NodeDesign root)
|
|
{
|
|
if (root.Children?.Items == null)
|
|
{
|
|
return false;
|
|
}
|
|
InstanceDesign parent = root.Children.Items.FirstOrDefault(x =>
|
|
current.RelativePath.StartsWith(x.SymbolicName.Name, StringComparison.Ordinal));
|
|
|
|
if (parent == null)
|
|
|
|
{
|
|
return false;
|
|
}
|
|
if (root is InstanceDesign instance &&
|
|
current.Instance is InstanceDesign child &&
|
|
child.ModellingRuleSpecified &&
|
|
child.ModellingRule != ModellingRule.Optional &&
|
|
child.ModellingRule != ModellingRule.Mandatory)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the identifiers from a CSV file.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
private SortedDictionary<object, IdInfo> AssignIdentifiers(
|
|
ModelDesign dictionary,
|
|
Dictionary<string, object> identifiers)
|
|
{
|
|
var uniqueIdentifiersComparer = Comparer<object>.Create((lhs, rhs) =>
|
|
{
|
|
Type lhst = lhs.GetType();
|
|
Type rhst = rhs.GetType();
|
|
if (lhst == rhst && lhs is IComparable c)
|
|
{
|
|
return c.CompareTo(rhs);
|
|
}
|
|
return lhst.Name.CompareTo(rhst.Name, StringComparison.Ordinal);
|
|
});
|
|
|
|
var uniqueIdentifiers =
|
|
new SortedDictionary<object, IdInfo>(uniqueIdentifiersComparer);
|
|
var duplicateIdentifiers = new Dictionary<string, object>();
|
|
var assignedIds = new IdAllocator(m_startId);
|
|
|
|
foreach (object existingId in identifiers.Values)
|
|
{
|
|
uint? numericId = existingId as uint?;
|
|
|
|
if (numericId != null)
|
|
|
|
{
|
|
assignedIds.Add(numericId.Value);
|
|
}
|
|
}
|
|
|
|
// Remove identifiers that are already known
|
|
foreach (XmlQualifiedName symbolicId in m_symbolicIdToNodeId.Keys)
|
|
{
|
|
identifiers.Remove(symbolicId.Name);
|
|
}
|
|
|
|
// assign identifiers.
|
|
for (int ii = 0; ii < dictionary.Items.Length; ii++)
|
|
{
|
|
NodeDesign node = dictionary.Items[ii];
|
|
|
|
// assign identifier if one has not already been assigned.
|
|
object id = AssignIdToNode(
|
|
node,
|
|
identifiers,
|
|
uniqueIdentifiers,
|
|
duplicateIdentifiers,
|
|
assignedIds,
|
|
false);
|
|
|
|
if (node.Hierarchy == null)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
foreach (HierarchyNode current in node.Hierarchy.NodeList)
|
|
{
|
|
if (string.IsNullOrEmpty(current.RelativePath))
|
|
{
|
|
current.Identifier = id;
|
|
|
|
// set identifier for node.
|
|
if (id is uint numericId)
|
|
{
|
|
current.Instance.NumericId = numericId;
|
|
current.Instance.NumericIdSpecified = true;
|
|
current.Instance.StringId = null;
|
|
}
|
|
else
|
|
{
|
|
current.Instance.NumericId = 0;
|
|
current.Instance.NumericIdSpecified = false;
|
|
current.Instance.StringId = id as string;
|
|
}
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Assigned ID: {Name}={Id}",
|
|
current.Instance.SymbolicId.Name,
|
|
id);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
bool isExplicitlyDefined =
|
|
(node is InstanceDesign &&
|
|
!current.Instance.SymbolicId.Name.Contains(
|
|
"Placeholder",
|
|
StringComparison.Ordinal)) ||
|
|
current.ExplicitlyDefined ||
|
|
IsExplicitlyDefined(current, node);
|
|
|
|
current.Identifier = AssignIdToNode(
|
|
current.Instance,
|
|
identifiers,
|
|
uniqueIdentifiers,
|
|
duplicateIdentifiers,
|
|
assignedIds,
|
|
!isExplicitlyDefined);
|
|
}
|
|
}
|
|
|
|
// check for duplicate nodes.
|
|
if (duplicateIdentifiers.Count > 0)
|
|
{
|
|
var buffer = new StringBuilder();
|
|
|
|
buffer.Append("Duplicate identifiers for these nodes:\r\n");
|
|
|
|
foreach (KeyValuePair<string, object> current in duplicateIdentifiers)
|
|
{
|
|
buffer.AppendFormat(
|
|
CultureInfo.InvariantCulture,
|
|
"{0},{1}\r\n",
|
|
current.Key,
|
|
current.Value);
|
|
}
|
|
|
|
throw new InvalidOperationException(buffer.ToString());
|
|
}
|
|
|
|
foreach (KeyValuePair<string, object> ii in identifiers)
|
|
{
|
|
if (!uniqueIdentifiers.ContainsKey(ii.Value))
|
|
{
|
|
uniqueIdentifiers[ii.Value] = new IdInfo
|
|
{
|
|
Id = ii.Value,
|
|
SymbolicId = ii.Key,
|
|
NodeClass = NodeClass.Unspecified,
|
|
ReleaseStatus = ReleaseStatus.Released
|
|
};
|
|
}
|
|
}
|
|
return uniqueIdentifiers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the identifiers from a CSV file.
|
|
/// </summary>
|
|
/// <exception cref="FileNotFoundException"></exception>
|
|
private void AssignIdentifiers(
|
|
ModelDesign dictionary,
|
|
string filePath = null)
|
|
{
|
|
Dictionary<string, object> identifiers;
|
|
if (string.IsNullOrEmpty(filePath) ||
|
|
!FileSystem.Exists(filePath))
|
|
{
|
|
identifiers = [];
|
|
}
|
|
else
|
|
{
|
|
using Stream istrm = FileSystem.OpenRead(filePath);
|
|
identifiers = ParseIdentifiersFromStream(istrm);
|
|
}
|
|
AssignIdentifiers(dictionary, identifiers);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports a node.
|
|
/// </summary>
|
|
private bool Import(ModelDesign model, NodeDesign node, NodeDesign parent)
|
|
{
|
|
UpdateNamesAndIdentifiers(node, parent);
|
|
|
|
if (node.NumericId != 0)
|
|
{
|
|
int ns = m_context.NamespaceUris.GetIndex(node.SymbolicId.Namespace);
|
|
var nodeId = new NodeId(node.NumericId, (ushort)ns);
|
|
m_nodesByNodeId[nodeId] = node;
|
|
m_symbolicIdToNodeId[node.SymbolicId] = nodeId;
|
|
node.NumericIdSpecified = true;
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(node.StringId))
|
|
{
|
|
int ns = m_context.NamespaceUris.GetIndex(node.SymbolicId.Namespace);
|
|
var nodeId = new NodeId(node.StringId, (ushort)ns);
|
|
m_nodesByNodeId[nodeId] = node;
|
|
m_symbolicIdToNodeId[node.SymbolicId] = nodeId;
|
|
}
|
|
|
|
if (IsExcluded(node))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// assign default values for various subtypes.
|
|
if (node is TypeDesign typeDesign)
|
|
{
|
|
ImportType(typeDesign);
|
|
|
|
if (node.SymbolicId.Namespace == Ua.Types.Namespaces.OpcUa)
|
|
|
|
{
|
|
node.Description = null;
|
|
}
|
|
}
|
|
|
|
if (node is InstanceDesign instanceDesign)
|
|
{
|
|
ImportInstance(instanceDesign);
|
|
|
|
if (parent != null && parent.SymbolicId.Namespace == Ua.Types.Namespaces.OpcUa)
|
|
|
|
{
|
|
node.Description = null;
|
|
}
|
|
}
|
|
|
|
m_nodes.Add(node.SymbolicId, node);
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Imported {Type}: {Name}",
|
|
node.GetType().Name,
|
|
node.SymbolicId.Name);
|
|
}
|
|
|
|
// import children.
|
|
if (node.Children != null && node.Children.Items != null)
|
|
{
|
|
node.HasChildren = true;
|
|
|
|
var children = new List<InstanceDesign>();
|
|
|
|
foreach (InstanceDesign child in node.Children.Items)
|
|
{
|
|
if (IsExcluded(child))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// filter any children with unhandled modelling rules.
|
|
if (child.ModellingRuleSpecified)
|
|
{
|
|
bool skip = false;
|
|
switch (child.ModellingRule)
|
|
{
|
|
case ModellingRule.None:
|
|
case ModellingRule.Mandatory:
|
|
case ModellingRule.MandatoryShared:
|
|
case ModellingRule.Optional:
|
|
case ModellingRule.MandatoryPlaceholder:
|
|
case ModellingRule.OptionalPlaceholder:
|
|
case ModellingRule.ExposesItsArray:
|
|
break;
|
|
default:
|
|
skip = true;
|
|
break;
|
|
}
|
|
if (skip)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (child.ReleaseStatus == ReleaseStatus.Released)
|
|
{
|
|
child.Category = node.Category;
|
|
child.ReleaseStatus = node.ReleaseStatus;
|
|
}
|
|
|
|
if (Import(model, child, node))
|
|
{
|
|
children.Add(child);
|
|
}
|
|
}
|
|
|
|
node.Children.Items = [.. children];
|
|
}
|
|
|
|
// import references
|
|
if (node.References != null)
|
|
{
|
|
node.HasReferences = true;
|
|
|
|
foreach (Reference reference in node.References)
|
|
{
|
|
ImportReference(node, reference);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the browse name for a node.
|
|
/// </summary>
|
|
private void SetBrowseName(NodeDesign node)
|
|
{
|
|
// use the name to assign a browse name.
|
|
if (string.IsNullOrEmpty(node.BrowseName))
|
|
{
|
|
if (!m_browseNames.TryGetValue(node.SymbolicName, out string browseName))
|
|
{
|
|
m_browseNames[node.SymbolicName] = browseName = node.SymbolicName.Name;
|
|
}
|
|
|
|
node.BrowseName = browseName;
|
|
}
|
|
else if (!m_browseNames.TryGetValue(node.SymbolicName, out string browseName))
|
|
{
|
|
m_browseNames[node.SymbolicName] = node.BrowseName;
|
|
}
|
|
else if (node.BrowseName != browseName)
|
|
{
|
|
throw Exception(
|
|
"The SymbolicName {0} has a BrowseName {1} but expected {2}.",
|
|
node.SymbolicName.Name,
|
|
browseName,
|
|
node.BrowseName);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures all the names and ids in the node have valid values.
|
|
/// </summary>
|
|
private void UpdateNamesAndIdentifiers(NodeDesign node, NodeDesign parent)
|
|
{
|
|
if (node == null)
|
|
{
|
|
return;
|
|
}
|
|
// copy the symbolic name and browse name from the declaration if specified.
|
|
|
|
if (node is InstanceDesign instance && !IsNull(instance.Declaration))
|
|
{
|
|
InstanceDesign declaration = this.FindNode<InstanceDesign>(
|
|
instance.Declaration,
|
|
instance.Declaration.Name,
|
|
"Declaration");
|
|
|
|
if (declaration != null)
|
|
{
|
|
instance.SymbolicName = declaration.SymbolicName;
|
|
instance.BrowseName = declaration.BrowseName;
|
|
instance.TypeDefinition = declaration.TypeDefinition;
|
|
}
|
|
}
|
|
|
|
// check for missing name.
|
|
if (IsNull(node.SymbolicId) &&
|
|
IsNull(node.SymbolicName) &&
|
|
string.IsNullOrEmpty(node.BrowseName))
|
|
{
|
|
throw Exception(
|
|
"A Node does not have SymbolicId, Name or a BrowseName. Parent={0}",
|
|
parent != null ? parent.SymbolicId.Name : "No Parent");
|
|
}
|
|
|
|
// use the browse name to assign a name.
|
|
if (IsNull(node.SymbolicName))
|
|
{
|
|
if (string.IsNullOrEmpty(node.BrowseName))
|
|
{
|
|
throw Exception(
|
|
"A Node does not have SymbolicId, SymbolicName or a BrowseName: {0}.",
|
|
node.SymbolicId.Name);
|
|
}
|
|
|
|
// remove any non-symbol characters.
|
|
var name = new StringBuilder();
|
|
|
|
for (int ii = 0; ii < node.BrowseName.Length; ii++)
|
|
{
|
|
if (char.IsWhiteSpace(node.BrowseName[ii]))
|
|
{
|
|
name.Append(NodeDesign.PathChar);
|
|
}
|
|
else if (char.IsLetterOrDigit(node.BrowseName[ii]) ||
|
|
node.BrowseName[ii] == NodeDesign.PathChar)
|
|
{
|
|
name.Append(node.BrowseName[ii]);
|
|
}
|
|
}
|
|
|
|
string ns = m_dictionary.TargetNamespace;
|
|
|
|
if (!IsNull(node.SymbolicId))
|
|
{
|
|
ns = node.SymbolicId.Namespace;
|
|
}
|
|
|
|
// create the symbolic name.
|
|
node.SymbolicName = new XmlQualifiedName(name.ToString(), ns);
|
|
}
|
|
|
|
// use the name to assign a browse name.
|
|
SetBrowseName(node);
|
|
|
|
// use the name to assign a symbolic id.
|
|
if (IsNull(node.SymbolicId))
|
|
{
|
|
node.SymbolicId = new XmlQualifiedName(
|
|
NodeDesign.CreateSymbolicId(
|
|
parent?.SymbolicId,
|
|
node.SymbolicName.Name),
|
|
node.SymbolicName.Namespace);
|
|
}
|
|
|
|
// check for duplicates.
|
|
if (m_nodes.ContainsKey(node.SymbolicId))
|
|
{
|
|
throw Exception(
|
|
"The SymbolicId is already used by another node: {0}.",
|
|
node.SymbolicId.Name);
|
|
}
|
|
|
|
// check numeric id.
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
if (m_identifiers.ContainsKey(node.NumericId))
|
|
{
|
|
throw Exception(
|
|
"The NumericId is already used by another node: {0}.",
|
|
node.NumericId);
|
|
}
|
|
|
|
m_identifiers.Add(node.NumericId, node);
|
|
}
|
|
|
|
// add a display name.
|
|
if (node.DisplayName == null)
|
|
{
|
|
node.DisplayName = new LocalizedText
|
|
{
|
|
Value = node.BrowseName,
|
|
IsAutogenerated = true
|
|
};
|
|
}
|
|
else if (node.DisplayName.Value != null)
|
|
{
|
|
node.DisplayName.Value = node.DisplayName.Value.Trim();
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(node.DisplayName.Key))
|
|
{
|
|
node.DisplayName.Key = CoreUtils.Format("{0}_DisplayName", node.SymbolicId.Name);
|
|
}
|
|
|
|
// add a decription.
|
|
if (node.Description?.Value != null && node.SymbolicId.Namespace != Ua.Types.Namespaces.OpcUa)
|
|
{
|
|
node.Description.Value = node.Description.Value.Trim();
|
|
|
|
if (string.IsNullOrEmpty(node.Description.Key))
|
|
{
|
|
node.Description.Key = CoreUtils.Format("{0}_Description", node.SymbolicId.Name);
|
|
}
|
|
}
|
|
|
|
// save the relationship to the parent.
|
|
node.Parent = parent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports an TypeDesign
|
|
/// </summary>
|
|
private void ImportType(TypeDesign type)
|
|
{
|
|
// assign a class name.
|
|
if (string.IsNullOrEmpty(type.ClassName))
|
|
{
|
|
type.ClassName = type.SymbolicName.Name;
|
|
|
|
if (type.ClassName.EndsWith("Type", StringComparison.Ordinal))
|
|
{
|
|
type.ClassName = type.ClassName[..^4];
|
|
}
|
|
}
|
|
|
|
// assign missing fields for object types.
|
|
|
|
if (type is ObjectTypeDesign objectType)
|
|
{
|
|
if (objectType.SymbolicId == s_baseObjectTypeQn)
|
|
{
|
|
objectType.ClassName = "ObjectSource";
|
|
}
|
|
else if (type.BaseType == null)
|
|
{
|
|
type.BaseType = s_baseObjectTypeQn;
|
|
}
|
|
|
|
if (objectType.SymbolicName != s_baseObjectTypeQn)
|
|
{
|
|
objectType.BaseTypeNode = this.FindNode<ObjectTypeDesign>(
|
|
objectType.BaseType,
|
|
objectType.SymbolicId.Name,
|
|
"BaseType");
|
|
}
|
|
|
|
if (!objectType.SupportsEvents)
|
|
|
|
{
|
|
objectType.SupportsEvents = false;
|
|
}
|
|
}
|
|
|
|
// assign missing fields for variable types.
|
|
|
|
if (type is VariableTypeDesign variableType)
|
|
{
|
|
if (variableType.SymbolicId == s_baseDataVariableTypeQn)
|
|
{
|
|
variableType.ClassName = "DataVariable";
|
|
}
|
|
else if (type.BaseType == null)
|
|
{
|
|
if (type.SymbolicId != new XmlQualifiedName("BaseVariableType", Ua.Types.Namespaces.OpcUa))
|
|
{
|
|
type.BaseType = s_baseDataVariableTypeQn;
|
|
}
|
|
}
|
|
|
|
if (variableType.SymbolicName != new XmlQualifiedName("BaseVariableType", Ua.Types.Namespaces.OpcUa))
|
|
{
|
|
variableType.BaseTypeNode = this.FindNode<VariableTypeDesign>(
|
|
variableType.BaseType,
|
|
variableType.SymbolicId.Name,
|
|
"BaseType");
|
|
|
|
if (variableType.BaseTypeNode != null)
|
|
{
|
|
if (variableType.DataType == null ||
|
|
variableType.DataType == s_baseDataTypeQn)
|
|
{
|
|
variableType.DataType = ((VariableTypeDesign)variableType.BaseTypeNode).DataType;
|
|
ValueRank valueRank = ((VariableTypeDesign)variableType.BaseTypeNode).ValueRank;
|
|
|
|
if (!variableType.ValueRankSpecified && valueRank != ValueRank.ScalarOrArray)
|
|
{
|
|
variableType.ValueRank = valueRank;
|
|
variableType.ValueRankSpecified = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variableType.DataType == null)
|
|
{
|
|
variableType.DataType = s_baseDataTypeQn;
|
|
|
|
for (var ii = variableType.BaseTypeNode as VariableTypeDesign;
|
|
ii != null;
|
|
ii = ii.BaseTypeNode as VariableTypeDesign)
|
|
{
|
|
if (ii.DataType != null)
|
|
{
|
|
variableType.DataType = ii.DataType;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!variableType.ValueRankSpecified)
|
|
{
|
|
variableType.ValueRank = ValueRank.Scalar;
|
|
|
|
for (var ii = variableType.BaseTypeNode as VariableTypeDesign;
|
|
ii != null;
|
|
ii = ii.BaseTypeNode as VariableTypeDesign)
|
|
{
|
|
if (ii.ValueRankSpecified)
|
|
{
|
|
variableType.ValueRank = ii.ValueRank;
|
|
variableType.ValueRankSpecified = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!variableType.AccessLevelSpecified)
|
|
|
|
{
|
|
variableType.AccessLevel = AccessLevel.Read;
|
|
}
|
|
if (!variableType.HistorizingSpecified)
|
|
|
|
{
|
|
variableType.Historizing = false;
|
|
}
|
|
if (!variableType.MinimumSamplingIntervalSpecified)
|
|
|
|
{
|
|
variableType.MinimumSamplingInterval = 0;
|
|
}
|
|
}
|
|
|
|
// assign missing fields for data types.
|
|
|
|
if (type is DataTypeDesign dataType)
|
|
{
|
|
if (dataType.SymbolicId == s_structureQn)
|
|
{
|
|
dataType.ClassName = "global::Opc.Ua.IEncodeable";
|
|
}
|
|
else if (type.BaseType == null)
|
|
{
|
|
if (dataType.SymbolicId != s_baseDataTypeQn)
|
|
{
|
|
type.BaseType = s_baseDataTypeQn;
|
|
}
|
|
}
|
|
|
|
if (dataType.SymbolicName != s_baseDataTypeQn)
|
|
{
|
|
dataType.BaseTypeNode = this.FindNode<DataTypeDesign>(
|
|
dataType.BaseType,
|
|
dataType.SymbolicId.Name,
|
|
"BaseType");
|
|
}
|
|
|
|
dataType.IsStructure = dataType.BaseType == s_structureQn;
|
|
dataType.IsEnumeration =
|
|
dataType.BaseType == s_enumerationQn ||
|
|
dataType.IsOptionSet;
|
|
dataType.IsUnion =
|
|
dataType.BaseType == s_unionQn ||
|
|
dataType.IsUnion;
|
|
|
|
Parameter[] parameters = dataType.Fields;
|
|
dataType.HasFields = ImportParameters(dataType, ref parameters, "Field");
|
|
dataType.Fields = parameters;
|
|
|
|
dataType.HasEncodings = ImportEncodings(dataType);
|
|
}
|
|
|
|
// assign missing fields for reference types.
|
|
|
|
if (type is ReferenceTypeDesign referenceType)
|
|
{
|
|
if (referenceType.BaseType == null &&
|
|
referenceType.SymbolicId != s_referencesQn)
|
|
{
|
|
referenceType.BaseType = s_referencesQn;
|
|
}
|
|
|
|
// add an inverse name.
|
|
referenceType.InverseName ??= new LocalizedText
|
|
{
|
|
Value = referenceType.DisplayName.Value,
|
|
IsAutogenerated = true
|
|
};
|
|
|
|
if (string.IsNullOrEmpty(referenceType.InverseName.Key))
|
|
{
|
|
referenceType.InverseName.Key = CoreUtils.Format(
|
|
"{0}_InverseName",
|
|
referenceType.SymbolicId.Name);
|
|
}
|
|
|
|
if (referenceType.SymbolicName != s_referencesQn)
|
|
{
|
|
referenceType.BaseTypeNode = this.FindNode<ReferenceTypeDesign>(
|
|
referenceType.BaseType,
|
|
referenceType.SymbolicId.Name,
|
|
"BaseType");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports the encodings.
|
|
/// </summary>
|
|
private bool ImportEncodings(DataTypeDesign dataType)
|
|
{
|
|
if (dataType.Encodings == null || dataType.Encodings.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
foreach (EncodingDesign encoding in dataType.Encodings)
|
|
{
|
|
if (IsNull(encoding.SymbolicName))
|
|
{
|
|
throw Exception(
|
|
"Encoding node does not have a name: {0}.",
|
|
dataType.SymbolicId.Name);
|
|
}
|
|
|
|
if (encoding.Children != null && encoding.Children.Items.Length > 0)
|
|
{
|
|
throw Exception(
|
|
"Encoding nodes cannot have childen",
|
|
dataType.SymbolicId.Name);
|
|
}
|
|
|
|
encoding.SymbolicId = new XmlQualifiedName(CoreUtils.Format(
|
|
"{0}_Encoding_{1}",
|
|
dataType.SymbolicId.Name,
|
|
encoding.SymbolicName.Name), dataType.SymbolicId.Namespace);
|
|
encoding.BrowseName = encoding.SymbolicName.Name;
|
|
|
|
// add a display name.
|
|
if (encoding.DisplayName == null ||
|
|
string.IsNullOrEmpty(encoding.DisplayName.Value))
|
|
{
|
|
encoding.DisplayName = new LocalizedText
|
|
{
|
|
Value = encoding.BrowseName,
|
|
IsAutogenerated = true
|
|
};
|
|
}
|
|
|
|
// add to table.
|
|
m_nodes.Add(encoding.SymbolicId, encoding);
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Imported {Type}: {Name}",
|
|
encoding.GetType().Name,
|
|
encoding.SymbolicId.Name);
|
|
}
|
|
|
|
if (encoding.NumericIdSpecified)
|
|
{
|
|
if (m_identifiers.ContainsKey(encoding.NumericId))
|
|
{
|
|
throw Exception(
|
|
"The NumericId is already used by another node: {0}.",
|
|
encoding.SymbolicId.Name);
|
|
}
|
|
|
|
m_identifiers.Add(encoding.NumericId, encoding);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports an InstanceDesign
|
|
/// </summary>
|
|
private void ImportInstance(InstanceDesign instance)
|
|
{
|
|
// set the reference type.
|
|
if (instance.ReferenceType == null)
|
|
{
|
|
if (instance is PropertyDesign)
|
|
{
|
|
instance.ReferenceType =
|
|
new XmlQualifiedName("HasProperty", Ua.Types.Namespaces.OpcUa);
|
|
}
|
|
else
|
|
{
|
|
instance.ReferenceType =
|
|
new XmlQualifiedName("HasComponent", Ua.Types.Namespaces.OpcUa);
|
|
}
|
|
}
|
|
|
|
// set the type definition.
|
|
if (instance.TypeDefinition == null)
|
|
{
|
|
if (instance is PropertyDesign)
|
|
{
|
|
instance.TypeDefinition =
|
|
new XmlQualifiedName("PropertyType", Ua.Types.Namespaces.OpcUa);
|
|
}
|
|
else if (instance is VariableDesign)
|
|
{
|
|
instance.TypeDefinition = s_baseDataVariableTypeQn;
|
|
}
|
|
else if (instance is ObjectDesign)
|
|
{
|
|
instance.TypeDefinition = s_baseObjectTypeQn;
|
|
}
|
|
}
|
|
|
|
if (!instance.ModellingRuleSpecified)
|
|
|
|
{
|
|
instance.ModellingRule = ModellingRule.Mandatory;
|
|
}
|
|
// assign missing fields for objects.
|
|
|
|
if (instance is ObjectDesign objectDesign &&
|
|
!objectDesign.SupportsEventsSpecified)
|
|
{
|
|
objectDesign.SupportsEvents = false;
|
|
}
|
|
|
|
// assign missing fields for variables.
|
|
|
|
if (instance is VariableDesign variable)
|
|
{
|
|
if (variable.DataType == null)
|
|
{
|
|
variable.DataType = s_baseDataTypeQn;
|
|
}
|
|
if (!variable.ValueRankSpecified)
|
|
|
|
{
|
|
variable.ValueRank = ValueRank.Scalar;
|
|
}
|
|
if (!variable.AccessLevelSpecified)
|
|
|
|
{
|
|
variable.AccessLevel = AccessLevel.Read;
|
|
}
|
|
if (!variable.MinimumSamplingIntervalSpecified)
|
|
|
|
{
|
|
variable.MinimumSamplingInterval = 0;
|
|
}
|
|
if (!variable.HistorizingSpecified)
|
|
|
|
{
|
|
variable.Historizing = false;
|
|
}
|
|
}
|
|
|
|
// assign missing fields for variables.
|
|
|
|
if (instance is MethodDesign method)
|
|
{
|
|
method.HasArguments = false;
|
|
|
|
if (!method.NonExecutableSpecified)
|
|
|
|
{
|
|
method.NonExecutableSpecified = false;
|
|
}
|
|
Parameter[] parameters = method.InputArguments;
|
|
|
|
if (ImportParameters(method, ref parameters, "InputArgument"))
|
|
{
|
|
method.HasArguments = true;
|
|
}
|
|
|
|
method.InputArguments = parameters;
|
|
|
|
parameters = method.OutputArguments;
|
|
|
|
if (ImportParameters(method, ref parameters, "OutputArgument"))
|
|
{
|
|
method.HasArguments = true;
|
|
}
|
|
|
|
method.OutputArguments = parameters;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a property placeholder for the arguments of a method.
|
|
/// </summary>
|
|
private PropertyDesign CreateArgumentProperty(MethodDesign method, string type)
|
|
{
|
|
var property = new PropertyDesign
|
|
{
|
|
Parent = method,
|
|
ReferenceType = new XmlQualifiedName("HasProperty", Ua.Types.Namespaces.OpcUa),
|
|
TypeDefinition = new XmlQualifiedName("PropertyType", Ua.Types.Namespaces.OpcUa),
|
|
SymbolicId = new XmlQualifiedName(
|
|
NodeDesign.CreateSymbolicId(method.SymbolicId.Name, type),
|
|
method.SymbolicId.Namespace),
|
|
SymbolicName = new XmlQualifiedName(type, Ua.Types.Namespaces.OpcUa)
|
|
};
|
|
|
|
// use the name to assign a browse name.
|
|
SetBrowseName(property);
|
|
|
|
property.AccessLevel = AccessLevel.Read;
|
|
property.ValueRank = ValueRank.Array;
|
|
property.DataType = new XmlQualifiedName("Argument", Ua.Types.Namespaces.OpcUa);
|
|
property.SetDefaultValue(null, m_context);
|
|
property.DisplayName = new LocalizedText
|
|
{
|
|
Value = property.BrowseName,
|
|
Key = CoreUtils.Format(
|
|
"{0}_(1)_DisplayName",
|
|
property.SymbolicId.Name,
|
|
type),
|
|
IsAutogenerated = true
|
|
};
|
|
property.Historizing = false;
|
|
property.MinimumSamplingInterval = 0;
|
|
property.ModellingRule = ModellingRule.Mandatory;
|
|
property.WriteAccess = 0;
|
|
|
|
property.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
property.DataType,
|
|
method.SymbolicId.Name,
|
|
"DataType");
|
|
|
|
property.TypeDefinitionNode = this.FindNode<VariableTypeDesign>(
|
|
property.TypeDefinition,
|
|
method.SymbolicId.Name,
|
|
"VariableType");
|
|
|
|
m_nodes.Add(property.SymbolicId, property);
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Imported {Type}: {Name}",
|
|
property.GetType().Name,
|
|
property.SymbolicId.Name);
|
|
}
|
|
|
|
return property;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports a list of parameters.
|
|
/// </summary>
|
|
private bool ImportParameters(
|
|
NodeDesign node,
|
|
ref Parameter[] parameters,
|
|
string parameterType)
|
|
{
|
|
if (parameters == null || parameters.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
decimal id = 0;
|
|
var filteredParameters = new List<Parameter>();
|
|
|
|
foreach (Parameter parameter in parameters)
|
|
{
|
|
if (IsExcluded(parameter))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
filteredParameters.Add(parameter);
|
|
|
|
parameter.Parent = node;
|
|
|
|
// check name.
|
|
if (string.IsNullOrEmpty(parameter.Name))
|
|
{
|
|
throw Exception(
|
|
"The node has a parameter without a name: {0}.",
|
|
node.SymbolicId.Name);
|
|
}
|
|
|
|
string name = parameter.Name;
|
|
|
|
// assign an id.
|
|
if (parameter.IdentifierSpecified)
|
|
{
|
|
id = parameter.Identifier;
|
|
}
|
|
else if (!string.IsNullOrEmpty(parameter.BitMask))
|
|
{
|
|
if (ulong.TryParse(
|
|
parameter.BitMask,
|
|
NumberStyles.AllowHexSpecifier,
|
|
CultureInfo.InvariantCulture,
|
|
out ulong mask))
|
|
{
|
|
byte[] bytes = BitConverter.GetBytes(mask);
|
|
parameter.Identifier = BitConverter.ToUInt64(bytes, 0);
|
|
parameter.IdentifierSpecified = true;
|
|
}
|
|
}
|
|
|
|
if (!parameter.IdentifierSpecified)
|
|
{
|
|
parameter.Identifier = ++id;
|
|
parameter.IdentifierSpecified = true;
|
|
}
|
|
|
|
// update id if specified in name.
|
|
int index = name.LastIndexOf(NodeDesign.PathChar);
|
|
|
|
if (index != -1)
|
|
{
|
|
for (int ii = index + 1; ii < name.Length; ii++)
|
|
{
|
|
if (!char.IsDigit(name[ii]))
|
|
{
|
|
index = -1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index > 0 && decimal.TryParse(name[(index + 1)..], out id))
|
|
{
|
|
parameter.Identifier = id;
|
|
parameter.IdentifierSpecified = true;
|
|
}
|
|
}
|
|
|
|
if (parameter.Description != null &&
|
|
string.IsNullOrEmpty(parameter.Description.Key))
|
|
{
|
|
parameter.Description.Key = CoreUtils.Format(
|
|
"{0}_{1}_{2}_Description",
|
|
node.SymbolicId.Name,
|
|
parameterType,
|
|
parameter.Name);
|
|
}
|
|
|
|
// add a datatype.
|
|
if (IsNull(parameter.DataType))
|
|
{
|
|
parameter.DataType = s_baseDataTypeQn;
|
|
}
|
|
}
|
|
|
|
parameters = [.. filteredParameters];
|
|
|
|
return parameters != null && parameters.Length != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports a reference.
|
|
/// </summary>
|
|
private void ImportReference(NodeDesign source, Reference reference)
|
|
{
|
|
reference.SourceNode = source;
|
|
|
|
if (IsNull(reference.TargetId))
|
|
{
|
|
throw Exception(
|
|
"The TargetId for a reference is not valid: {0}.",
|
|
source.SymbolicId.Name);
|
|
}
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Import Reference: {Source} => {Reference} => {Target}",
|
|
reference.SourceNode.SymbolicName.Name,
|
|
reference.ReferenceType.Name,
|
|
reference.TargetId.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a node.
|
|
/// </summary>
|
|
private void Validate(NodeDesign node)
|
|
{
|
|
if (IsDeclaration(node))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (node is TypeDesign typeDesign)
|
|
|
|
{
|
|
ValidateType(typeDesign);
|
|
}
|
|
if (node is InstanceDesign instanceDesign)
|
|
|
|
{
|
|
ValidateInstance(instanceDesign);
|
|
}
|
|
if (node.HasChildren)
|
|
{
|
|
foreach (NodeDesign child in node.Children.Items)
|
|
{
|
|
Validate(child);
|
|
}
|
|
}
|
|
|
|
if (node.HasReferences)
|
|
{
|
|
var references = new List<Reference>();
|
|
|
|
foreach (Reference reference in node.References)
|
|
{
|
|
try
|
|
{
|
|
ValidateReference(reference);
|
|
references.Add(reference);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogWarning(
|
|
e,
|
|
"Ignoring InvalidReference {Name} => {Target}",
|
|
node.SymbolicId.Name,
|
|
reference.TargetId.Name);
|
|
}
|
|
}
|
|
|
|
node.References = [.. references];
|
|
|
|
// Auto-set SupportsEvents when the node has forward HasEventSource
|
|
// or HasNotifier references. Per OPC UA Part 3, the EventNotifier
|
|
// attribute must be set on the source node of these references.
|
|
if (node is ObjectDesign objectNode &&
|
|
!objectNode.SupportsEvents &&
|
|
!objectNode.SupportsEventsSpecified)
|
|
{
|
|
foreach (Reference reference in references)
|
|
{
|
|
if (!reference.IsInverse &&
|
|
reference.ReferenceType != null &&
|
|
(reference.ReferenceType.Name == "HasEventSource" ||
|
|
reference.ReferenceType.Name == "HasNotifier"))
|
|
{
|
|
objectNode.SupportsEvents = true;
|
|
objectNode.SupportsEventsSpecified = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (node is ObjectTypeDesign objectTypeNode &&
|
|
!objectTypeNode.SupportsEvents &&
|
|
!objectTypeNode.SupportsEventsSpecified)
|
|
{
|
|
foreach (Reference reference in references)
|
|
{
|
|
if (!reference.IsInverse &&
|
|
reference.ReferenceType != null &&
|
|
(reference.ReferenceType.Name == "HasEventSource" ||
|
|
reference.ReferenceType.Name == "HasNotifier"))
|
|
{
|
|
objectTypeNode.SupportsEvents = true;
|
|
objectTypeNode.SupportsEventsSpecified = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
InjectStateMachineGeneratesEventReference(node);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Issue #3720 — Per OPC UA Part 16 §4.4.5 / Part 5 §6.4.2, instances
|
|
/// of StateMachineType and FiniteStateMachineType emit
|
|
/// AuditUpdateStateEventType events on state changes. The standard
|
|
/// design XML omits the corresponding GeneratesEvent reference, so
|
|
/// inject it programmatically here. Idempotent: skipped when the
|
|
/// reference (or a subtype like AlwaysGeneratesEvent) is already
|
|
/// declared. The injected reference is fully validated (TargetNode
|
|
/// resolved) so downstream NodeId emission matches the standard
|
|
/// reference flow.
|
|
/// </summary>
|
|
private void InjectStateMachineGeneratesEventReference(NodeDesign node)
|
|
{
|
|
if (node is not ObjectTypeDesign objectType ||
|
|
objectType.SymbolicName == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string name = objectType.SymbolicName.Name;
|
|
if (!string.Equals(name, "StateMachineType", StringComparison.Ordinal) &&
|
|
!string.Equals(name, "FiniteStateMachineType", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (objectType.References != null)
|
|
{
|
|
foreach (Reference existing in objectType.References)
|
|
{
|
|
if (existing.IsInverse ||
|
|
existing.ReferenceType == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.Equals(existing.ReferenceType.Name, "GeneratesEvent", StringComparison.Ordinal) ||
|
|
string.Equals(existing.ReferenceType.Name, "AlwaysGeneratesEvent", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
var injected = new Reference
|
|
{
|
|
SourceNode = objectType,
|
|
ReferenceType = new XmlQualifiedName(
|
|
"GeneratesEvent",
|
|
Ua.Types.Namespaces.OpcUa),
|
|
IsInverse = false,
|
|
IsOneWay = true,
|
|
TargetId = new XmlQualifiedName(
|
|
"TransitionEventType",
|
|
Ua.Types.Namespaces.OpcUa)
|
|
};
|
|
|
|
ValidateReference(injected);
|
|
|
|
objectType.References = objectType.References == null
|
|
? [injected]
|
|
: [.. objectType.References, injected];
|
|
objectType.HasReferences = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the type.
|
|
/// </summary>
|
|
private void ValidateType(TypeDesign type)
|
|
{
|
|
// assign missing fields for object types.
|
|
|
|
/*
|
|
if (type is ObjectTypeDesign objectType)
|
|
{
|
|
if (objectType.SymbolicName != s_baseObjectTypeQn)
|
|
{
|
|
objectType.BaseTypeNode = FindNode<ObjectTypeDesign>(
|
|
objectType.BaseType,
|
|
objectType.SymbolicId.Name,
|
|
"BaseType");
|
|
}
|
|
}
|
|
*/
|
|
|
|
// assign missing fields for variable types.
|
|
|
|
if (type is VariableTypeDesign variableType)
|
|
{
|
|
/*
|
|
if (variableType.SymbolicName != s_baseVariableTypeQn)
|
|
{
|
|
variableType.BaseTypeNode = FindNode<VariableTypeDesign>(
|
|
variableType.BaseType,
|
|
variableType.SymbolicId.Name,
|
|
"BaseType");
|
|
|
|
if (variableType.BaseTypeNode != null)
|
|
{
|
|
if (variableType.DataType == null ||
|
|
variableType.DataType == s_baseDataTypeQn)
|
|
{
|
|
variableType.DataType = ((VariableTypeDesign)variableType.BaseTypeNode).DataType;
|
|
ValueRank valueRank = ((VariableTypeDesign)variableType.BaseTypeNode).ValueRank;
|
|
|
|
if (!variableType.ValueRankSpecified && valueRank != ValueRank.ScalarOrArray)
|
|
{
|
|
variableType.ValueRank = valueRank;
|
|
variableType.ValueRankSpecified = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
variableType.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
variableType.DataType,
|
|
type.SymbolicId.Name,
|
|
"DataType");
|
|
|
|
if (variableType.DefaultValue != null)
|
|
{
|
|
var decoder = new XmlDecoder(variableType.DefaultValue, m_context);
|
|
Variant variant = decoder.ReadVariantValue(null, default);
|
|
|
|
if (!variant.TypeInfo.IsUnknown)
|
|
{
|
|
variableType.DecodedValue = variant.AsBoxedObject(Variant.BoxingBehavior.Legacy);
|
|
variableType.ValueRank = variant.TypeInfo.ValueRank == ValueRanks.Scalar ?
|
|
ValueRank.Scalar :
|
|
ValueRank.Array;
|
|
variableType.ValueRankSpecified = true;
|
|
}
|
|
|
|
decoder.Close();
|
|
}
|
|
|
|
if (variableType.BaseTypeNode != null)
|
|
{
|
|
var baseType = variableType.BaseTypeNode as VariableTypeDesign;
|
|
|
|
if (baseType.DataType != s_baseDataTypeQn)
|
|
{
|
|
if (variableType.DataType == s_baseDataTypeQn)
|
|
{
|
|
variableType.DataType = baseType.DataType;
|
|
variableType.DataTypeNode = baseType.DataTypeNode;
|
|
}
|
|
|
|
if (baseType.DataType != variableType.DataType)
|
|
{
|
|
XmlQualifiedName ii = variableType.DataTypeNode.BaseType;
|
|
|
|
if (ii != null && ii != baseType.DataType)
|
|
{
|
|
DataTypeDesign parent = this.FindNode<DataTypeDesign>(
|
|
ii,
|
|
variableType.SymbolicId.Name,
|
|
"DataType");
|
|
|
|
ii = parent.BaseType;
|
|
}
|
|
|
|
if (ii != baseType.DataType)
|
|
{
|
|
throw Exception(
|
|
"The VariableType subtype cannot redefine the datatype. {0}",
|
|
type.SymbolicId.Name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// assign missing fields for data types.
|
|
|
|
if (type is DataTypeDesign dataType)
|
|
{
|
|
/*
|
|
if (dataType.SymbolicName != s_baseDataTypeQn)
|
|
{
|
|
dataType.BaseTypeNode = FindNode<DataTypeDesign>(
|
|
dataType.BaseType,
|
|
dataType.SymbolicId.Name,
|
|
"BaseType");
|
|
}
|
|
*/
|
|
|
|
ValidateParameters(dataType, dataType.Fields);
|
|
|
|
dataType.IsStructure = IsTypeOf(
|
|
dataType,
|
|
s_structureQn);
|
|
dataType.IsEnumeration = IsTypeOf(
|
|
dataType,
|
|
s_enumerationQn) ||
|
|
dataType.IsOptionSet;
|
|
dataType.BasicDataType = dataType.DetermineBasicDataType();
|
|
|
|
if (!dataType.IsStructure)
|
|
{
|
|
if (dataType.HasEncodings)
|
|
{
|
|
throw Exception(
|
|
"The datatype has encodings but does not inherit from a structure: {0}",
|
|
type.SymbolicId.Name);
|
|
}
|
|
|
|
if (dataType.IsEnumeration)
|
|
{
|
|
if (!dataType.HasFields && !dataType.IsAbstract)
|
|
{
|
|
throw Exception(
|
|
"The datatype is an enumeration with no fields: {0}",
|
|
type.SymbolicId.Name);
|
|
}
|
|
}
|
|
else if (dataType.HasFields && !dataType.IsOptionSet)
|
|
{
|
|
throw Exception(
|
|
"The datatype is a simple type but it has fields defined: {0}",
|
|
type.SymbolicId.Name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// add the default encodings.
|
|
if (!dataType.HasEncodings)
|
|
{
|
|
EncodingDesign xmlEncoding = CreateEncoding(
|
|
dataType,
|
|
new XmlQualifiedName("DefaultXml", Ua.Types.Namespaces.OpcUa));
|
|
EncodingDesign binaryEncoding = CreateEncoding(
|
|
dataType,
|
|
new XmlQualifiedName("DefaultBinary", Ua.Types.Namespaces.OpcUa));
|
|
|
|
dataType.Encodings = [xmlEncoding, binaryEncoding];
|
|
|
|
dataType.HasEncodings = true;
|
|
}
|
|
|
|
// check for duplicates.
|
|
var encodings = new Dictionary<XmlQualifiedName, EncodingDesign>();
|
|
|
|
foreach (EncodingDesign encoding in dataType.Encodings)
|
|
{
|
|
if (encodings.ContainsKey(encoding.SymbolicName))
|
|
{
|
|
throw Exception(
|
|
"The datatype has a duplicate encoding defined: {0} {1}",
|
|
dataType.SymbolicId.Name,
|
|
encoding.SymbolicName.Name);
|
|
}
|
|
|
|
encodings.Add(encoding.SymbolicName, encoding);
|
|
}
|
|
}
|
|
}
|
|
|
|
// assign missing fields for references types.
|
|
/*
|
|
if (type is ReferenceTypeDesign referenceType)
|
|
{
|
|
if (referenceType.SymbolicName != s_referencesQn)
|
|
{
|
|
referenceType.BaseTypeNode = FindNode<ReferenceTypeDesign>(
|
|
referenceType.BaseType,
|
|
referenceType.SymbolicId.Name,
|
|
"BaseType");
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports an InstanceDesign
|
|
/// </summary>
|
|
private void ValidateInstance(InstanceDesign instance)
|
|
{
|
|
// set the reference type.
|
|
if (instance.ReferenceType == null && instance.Parent != null)
|
|
{
|
|
ReferenceTypeDesign referenceType = this.FindNode<ReferenceTypeDesign>(
|
|
instance.ReferenceType,
|
|
instance.SymbolicId.Name,
|
|
"ReferenceType");
|
|
|
|
if (referenceType == null)
|
|
{
|
|
m_logger.LogWarning("Reference type {Name} not found", instance.ReferenceType);
|
|
}
|
|
}
|
|
|
|
// assign missing fields for object.
|
|
|
|
if (instance is ObjectDesign objectd)
|
|
{
|
|
objectd.TypeDefinitionNode = this.FindNode<ObjectTypeDesign>(
|
|
instance.TypeDefinition,
|
|
instance.SymbolicId.Name,
|
|
"TypeDefinition");
|
|
|
|
//if (!objectd.ModellingRuleSpecified || objectd.ModellingRule == ModellingRule.None)
|
|
//{
|
|
// if (objectd.RolePermissions == null)
|
|
// {
|
|
// objectd.RolePermissions = objectd.TypeDefinitionNode.DefaultRolePermissions;
|
|
// }
|
|
|
|
// if (!objectd.AccessRestrictionsSpecified)
|
|
// {
|
|
// objectd.AccessRestrictions = objectd.TypeDefinitionNode.DefaultAccessRestrictions;
|
|
// objectd.AccessRestrictionsSpecified = objectd.TypeDefinitionNode.DefaultAccessRestrictionsSpecified;
|
|
// }
|
|
//}
|
|
//else
|
|
//{
|
|
// if (objectd.DefaultRolePermissions == null)
|
|
// {
|
|
// objectd.DefaultRolePermissions = objectd.TypeDefinitionNode.DefaultRolePermissions;
|
|
// }
|
|
|
|
// if (!objectd.DefaultAccessRestrictionsSpecified)
|
|
// {
|
|
// objectd.DefaultAccessRestrictions = objectd.TypeDefinitionNode.DefaultAccessRestrictions;
|
|
// objectd.DefaultAccessRestrictionsSpecified = objectd.TypeDefinitionNode.DefaultAccessRestrictionsSpecified;
|
|
// }
|
|
//}
|
|
}
|
|
|
|
// assign missing fields for variables.
|
|
|
|
if (instance is VariableDesign variable)
|
|
{
|
|
if (variable is PropertyDesign && variable.HasChildren)
|
|
{
|
|
throw Exception("The Property ({0}) has children defined.", variable.SymbolicId.Name);
|
|
}
|
|
|
|
variable.TypeDefinitionNode = this.FindNode<VariableTypeDesign>(
|
|
instance.TypeDefinition,
|
|
instance.SymbolicId.Name,
|
|
"TypeDefinition");
|
|
|
|
if (variable.TypeDefinitionNode != null)
|
|
{
|
|
if (variable.DataType == null ||
|
|
variable.DataType == s_baseDataTypeQn)
|
|
{
|
|
variable.DataType = ((VariableTypeDesign)variable.TypeDefinitionNode).DataType;
|
|
|
|
ValueRank valueRank = ((VariableTypeDesign)variable.TypeDefinitionNode).ValueRank;
|
|
|
|
if (!variable.ValueRankSpecified && valueRank != ValueRank.ScalarOrArray)
|
|
{
|
|
variable.ValueRank = valueRank;
|
|
variable.ValueRankSpecified = true;
|
|
}
|
|
}
|
|
|
|
//if (!variable.ModellingRuleSpecified || variable.ModellingRule == ModellingRule.None)
|
|
//{
|
|
// if (variable.RolePermissions == null)
|
|
// {
|
|
// variable.RolePermissions = variable.TypeDefinitionNode.DefaultRolePermissions;
|
|
// }
|
|
|
|
// if (!variable.AccessRestrictionsSpecified)
|
|
// {
|
|
// variable.AccessRestrictions = variable.TypeDefinitionNode.DefaultAccessRestrictions;
|
|
// variable.AccessRestrictionsSpecified = variable.TypeDefinitionNode.DefaultAccessRestrictionsSpecified;
|
|
// }
|
|
//}
|
|
//else
|
|
//{
|
|
// if (variable.DefaultRolePermissions == null)
|
|
// {
|
|
// variable.DefaultRolePermissions = variable.TypeDefinitionNode.DefaultRolePermissions;
|
|
// }
|
|
|
|
// if (!variable.DefaultAccessRestrictionsSpecified)
|
|
// {
|
|
// variable.DefaultAccessRestrictions = variable.TypeDefinitionNode.DefaultAccessRestrictions;
|
|
// variable.DefaultAccessRestrictionsSpecified = variable.TypeDefinitionNode.DefaultAccessRestrictionsSpecified;
|
|
// }
|
|
//}
|
|
}
|
|
|
|
variable.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
variable.DataType,
|
|
instance.SymbolicId.Name,
|
|
"DataType");
|
|
|
|
if (variable.DefaultValue != null)
|
|
{
|
|
var decoder = new XmlDecoder(variable.DefaultValue, m_context);
|
|
Variant variant = decoder.ReadVariantValue(null, default);
|
|
if (!variant.TypeInfo.IsUnknown)
|
|
{
|
|
variable.ValueRank =
|
|
variant.TypeInfo.ValueRank == ValueRanks.Scalar ? ValueRank.Scalar : ValueRank.Array;
|
|
variable.ValueRankSpecified = true;
|
|
variable.DecodedValue = variant.AsBoxedObject(Variant.BoxingBehavior.Legacy);
|
|
}
|
|
|
|
decoder.Close();
|
|
}
|
|
}
|
|
|
|
// assign missing fields for methods.
|
|
|
|
if (instance is MethodDesign method)
|
|
{
|
|
if (instance.TypeDefinition != null)
|
|
{
|
|
method.MethodType = this.FindNode<MethodDesign>(
|
|
instance.TypeDefinition,
|
|
instance.SymbolicId.Name,
|
|
"TypeDefinition");
|
|
|
|
method.Description = method.MethodType.Description;
|
|
method.InputArguments = method.MethodType.InputArguments;
|
|
method.OutputArguments = method.MethodType.OutputArguments;
|
|
method.HasArguments = (method.InputArguments != null && method.InputArguments.Length > 0) ||
|
|
(method.OutputArguments != null && method.OutputArguments.Length > 0);
|
|
|
|
//if (!method.ModellingRuleSpecified || method.ModellingRule == ModellingRule.None)
|
|
//{
|
|
// if (method.RolePermissions == null)
|
|
// {
|
|
// method.RolePermissions = method.MethodType.DefaultRolePermissions;
|
|
// }
|
|
|
|
// if (!method.AccessRestrictionsSpecified)
|
|
// {
|
|
// method.AccessRestrictions = method.MethodType.DefaultAccessRestrictions;
|
|
// method.AccessRestrictionsSpecified = method.MethodType.DefaultAccessRestrictionsSpecified;
|
|
// }
|
|
//}
|
|
//else
|
|
//{
|
|
// if (method.DefaultRolePermissions == null)
|
|
// {
|
|
// method.DefaultRolePermissions = method.MethodType.DefaultRolePermissions;
|
|
// }
|
|
|
|
// if (!method.DefaultAccessRestrictionsSpecified)
|
|
// {
|
|
// method.DefaultAccessRestrictions = method.MethodType.DefaultAccessRestrictions;
|
|
// method.DefaultAccessRestrictionsSpecified = method.MethodType.DefaultAccessRestrictionsSpecified;
|
|
// }
|
|
//}
|
|
}
|
|
|
|
ValidateParameters(method, method.InputArguments);
|
|
ValidateParameters(method, method.OutputArguments);
|
|
|
|
if (method.Parent != null)
|
|
{
|
|
var children = new List<InstanceDesign>();
|
|
|
|
if (method.Children != null && method.Children.Items != null)
|
|
|
|
{
|
|
children.AddRange(method.Children.Items);
|
|
}
|
|
if (method.InputArguments != null)
|
|
|
|
{
|
|
children.Add(CreateArgumentProperty(method, "InputArguments"));
|
|
}
|
|
if (method.OutputArguments != null)
|
|
|
|
{
|
|
children.Add(CreateArgumentProperty(method, "OutputArguments"));
|
|
}
|
|
if (children.Count > 0)
|
|
{
|
|
method.Children = new ListOfChildren
|
|
{
|
|
Items = [.. children]
|
|
};
|
|
method.HasChildren = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ValidateParameters(NodeDesign node, Parameter[] parameters)
|
|
{
|
|
if (parameters != null)
|
|
{
|
|
foreach (Parameter parameter in parameters)
|
|
{
|
|
parameter.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
parameter.DataType,
|
|
node.SymbolicId.Name,
|
|
"DataType");
|
|
|
|
if (IsTypeOf(parameter.DataTypeNode, s_structureQn) &&
|
|
parameter.AllowSubTypes &&
|
|
!UseAllowSubtypes)
|
|
{
|
|
parameter.DataTypeNode = this.FindNode<DataTypeDesign>(
|
|
s_structureQn,
|
|
node.SymbolicId.Name,
|
|
"DataType");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ValidateReference(Reference reference)
|
|
{
|
|
ReferenceTypeDesign referenceType = this.FindNode<ReferenceTypeDesign>(
|
|
reference.ReferenceType,
|
|
reference.SourceNode.SymbolicId.Name,
|
|
"ReferenceType");
|
|
|
|
if (referenceType == null)
|
|
{
|
|
m_logger.LogWarning("Reference type {Name} not found", reference.ReferenceType);
|
|
}
|
|
|
|
reference.TargetNode = this.FindNode(
|
|
reference.TargetId,
|
|
reference.SourceNode.SymbolicId.Name,
|
|
"TargetId");
|
|
}
|
|
|
|
private bool IsTypeOf(TypeDesign type, XmlQualifiedName superType)
|
|
{
|
|
if (type.SymbolicId == superType)
|
|
{
|
|
return true;
|
|
}
|
|
if (IsNull(type.BaseType))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!m_nodes.TryGetValue(type.BaseType, out NodeDesign node))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return IsTypeOf(node as TypeDesign, superType);
|
|
}
|
|
|
|
private EncodingDesign CreateEncoding(DataTypeDesign dataType, XmlQualifiedName encodingName)
|
|
{
|
|
var symbolicId = new XmlQualifiedName(
|
|
CoreUtils.Format("{0}_Encoding_{1}", dataType.SymbolicId.Name, encodingName.Name),
|
|
dataType.SymbolicId.Namespace);
|
|
|
|
var encoding = new EncodingDesign
|
|
{
|
|
SymbolicName = encodingName,
|
|
SymbolicId = symbolicId,
|
|
ReleaseStatus = dataType.ReleaseStatus,
|
|
Purpose = dataType.Purpose
|
|
};
|
|
|
|
if (m_nodes.TryGetValue(symbolicId, out NodeDesign target))
|
|
{
|
|
encoding.NumericId = target.NumericId;
|
|
encoding.NumericIdSpecified = target.NumericIdSpecified;
|
|
m_nodes.Remove(symbolicId);
|
|
}
|
|
|
|
// use the name to assign a browse name.
|
|
SetBrowseName(encoding);
|
|
|
|
encoding.TypeDefinition = new XmlQualifiedName("DataTypeEncodingType", Ua.Types.Namespaces.OpcUa);
|
|
encoding.Parent = dataType;
|
|
|
|
encoding.TypeDefinitionNode = this.FindNode<ObjectTypeDesign>(
|
|
encoding.TypeDefinition,
|
|
encoding.SymbolicId.Name,
|
|
"DataTypeEncodingType");
|
|
|
|
// add a display name.
|
|
if (encoding.DisplayName == null ||
|
|
string.IsNullOrEmpty(encoding.DisplayName.Value))
|
|
{
|
|
encoding.DisplayName = new LocalizedText
|
|
{
|
|
Value = encoding.BrowseName,
|
|
IsAutogenerated = true
|
|
};
|
|
}
|
|
|
|
m_nodes.Add(encoding.SymbolicId, encoding);
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Created {Type}: {Name}",
|
|
encoding.GetType().Name,
|
|
encoding.SymbolicId.Name);
|
|
}
|
|
|
|
return encoding;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get node class of design element
|
|
/// </summary>
|
|
private static NodeClass GetNodeClass(NodeDesign node)
|
|
{
|
|
if (node is ObjectDesign)
|
|
{
|
|
return NodeClass.Object;
|
|
}
|
|
if (node is ObjectTypeDesign)
|
|
|
|
{
|
|
return NodeClass.ObjectType;
|
|
}
|
|
if (node is DataTypeDesign)
|
|
|
|
{
|
|
return NodeClass.DataType;
|
|
}
|
|
if (node is ReferenceTypeDesign)
|
|
|
|
{
|
|
return NodeClass.ReferenceType;
|
|
}
|
|
if (node is MethodDesign)
|
|
|
|
{
|
|
return NodeClass.Method;
|
|
}
|
|
if (node is VariableDesign)
|
|
|
|
{
|
|
return NodeClass.Variable;
|
|
}
|
|
if (node is VariableTypeDesign)
|
|
|
|
{
|
|
return NodeClass.VariableType;
|
|
}
|
|
if (node is ViewDesign)
|
|
|
|
{
|
|
return NodeClass.View;
|
|
}
|
|
return NodeClass.Unspecified;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the value rank enumeration onto a integer.
|
|
/// </summary>
|
|
private static int ConstructValueRank(ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Array:
|
|
return ValueRanks.OneDimension;
|
|
case ValueRank.Scalar:
|
|
return ValueRanks.Scalar;
|
|
case ValueRank.Any:
|
|
case ValueRank.ScalarOrArray:
|
|
return ValueRanks.Any;
|
|
case ValueRank.ScalarOrOneDimension:
|
|
return ValueRanks.ScalarOrOneDimension;
|
|
case ValueRank.OneOrMoreDimensions:
|
|
if (string.IsNullOrEmpty(arrayDimensions))
|
|
{
|
|
return ValueRanks.OneOrMoreDimensions;
|
|
}
|
|
|
|
string[] dimensions = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
return dimensions.Length;
|
|
}
|
|
|
|
return ValueRanks.Any;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the array dimensions onto a constant declaration..
|
|
/// </summary>
|
|
private static ArrayOf<uint> ConstructArrayDimensionsRW(ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
if (valueRank is < 0 and not ValueRank.OneOrMoreDimensions)
|
|
{
|
|
return default;
|
|
}
|
|
if (string.IsNullOrEmpty(arrayDimensions))
|
|
{
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return [.. new uint[1]];
|
|
}
|
|
return default;
|
|
}
|
|
|
|
string[] tokens = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (tokens == null || tokens.Length < 1)
|
|
|
|
{
|
|
return default;
|
|
}
|
|
var dimensions = new List<uint>();
|
|
|
|
for (int ii = 0; ii < tokens.Length; ii++)
|
|
{
|
|
try
|
|
{
|
|
dimensions.Add(Convert.ToUInt32(tokens[ii], CultureInfo.InvariantCulture));
|
|
}
|
|
catch
|
|
{
|
|
dimensions.Add(0);
|
|
}
|
|
}
|
|
|
|
return dimensions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the browse path to the instance.
|
|
/// </summary>
|
|
private static string GetBrowsePath(string basePath, InstanceDesign instance)
|
|
{
|
|
return NodeDesign.CreateSymbolicId(basePath, instance.SymbolicName.Name);
|
|
}
|
|
|
|
private TypeDesign MergeTypeHierarchy(TypeDesign type)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug("Merging Type: {Name}", type.SymbolicId.Name);
|
|
}
|
|
|
|
TypeDesign mergedType;
|
|
if (type.BaseTypeNode == null)
|
|
{
|
|
mergedType = type.Copy();
|
|
|
|
mergedType.NumericId = 0;
|
|
mergedType.NumericIdSpecified = false;
|
|
mergedType.StringId = null;
|
|
}
|
|
else
|
|
{
|
|
mergedType = MergeTypeHierarchy(type.BaseTypeNode);
|
|
MergeTypes(mergedType, type);
|
|
}
|
|
return mergedType;
|
|
}
|
|
|
|
private static void MergeTypes(
|
|
TypeDesign mergedType,
|
|
TypeDesign type)
|
|
{
|
|
mergedType.SymbolicId = type.SymbolicId;
|
|
mergedType.SymbolicName = type.SymbolicName;
|
|
mergedType.NumericId = type.NumericId;
|
|
mergedType.NumericIdSpecified = type.NumericIdSpecified;
|
|
mergedType.StringId = type.StringId;
|
|
mergedType.ClassName = type.ClassName;
|
|
mergedType.BrowseName = type.BrowseName;
|
|
mergedType.DisplayName = type.DisplayName;
|
|
mergedType.Description = type.Description;
|
|
mergedType.BaseType = type.BaseType;
|
|
mergedType.BaseTypeNode = type.BaseTypeNode;
|
|
mergedType.IsAbstract = type.IsAbstract;
|
|
mergedType.Children = null;
|
|
mergedType.References = null;
|
|
mergedType.Category = type.Category;
|
|
mergedType.Purpose = type.Purpose;
|
|
mergedType.ReleaseStatus = type.ReleaseStatus;
|
|
mergedType.RolePermissions = type.RolePermissions;
|
|
mergedType.AccessRestrictions = type.AccessRestrictions;
|
|
mergedType.AccessRestrictionsSpecified = type.AccessRestrictionsSpecified;
|
|
mergedType.DefaultRolePermissions = type.DefaultRolePermissions;
|
|
mergedType.DefaultAccessRestrictions = type.DefaultAccessRestrictions;
|
|
mergedType.DefaultAccessRestrictionsSpecified = type.DefaultAccessRestrictionsSpecified;
|
|
mergedType.WriteAccess = type.WriteAccess;
|
|
|
|
if (type is VariableTypeDesign variableType)
|
|
|
|
{
|
|
MergeTypes((VariableTypeDesign)mergedType, variableType);
|
|
}
|
|
if (type is ObjectTypeDesign objectType)
|
|
|
|
{
|
|
MergeTypes((ObjectTypeDesign)mergedType, objectType);
|
|
}
|
|
}
|
|
|
|
private static void MergeTypes(
|
|
VariableTypeDesign mergedType,
|
|
VariableTypeDesign variableType)
|
|
{
|
|
if (variableType.DecodedValue != null)
|
|
{
|
|
mergedType.DecodedValue = variableType.DecodedValue;
|
|
}
|
|
if (variableType.DefaultValue != null)
|
|
{
|
|
mergedType.DefaultValue = variableType.DefaultValue;
|
|
}
|
|
if (variableType.DataType != null &&
|
|
variableType.DataType != s_baseDataTypeQn)
|
|
{
|
|
mergedType.DataType = variableType.DataType;
|
|
mergedType.DataTypeNode = variableType.DataTypeNode;
|
|
}
|
|
|
|
if (variableType.ValueRankSpecified)
|
|
{
|
|
mergedType.ValueRank = variableType.ValueRank;
|
|
mergedType.ValueRankSpecified = true;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(variableType.ArrayDimensions))
|
|
{
|
|
mergedType.ArrayDimensions = variableType.ArrayDimensions;
|
|
}
|
|
|
|
if (variableType.AccessLevelSpecified)
|
|
{
|
|
mergedType.AccessLevel = variableType.AccessLevel;
|
|
mergedType.AccessLevelSpecified = true;
|
|
}
|
|
|
|
if (variableType.MinimumSamplingIntervalSpecified)
|
|
{
|
|
mergedType.MinimumSamplingInterval = variableType.MinimumSamplingInterval;
|
|
mergedType.MinimumSamplingIntervalSpecified = true;
|
|
}
|
|
|
|
if (variableType.HistorizingSpecified)
|
|
{
|
|
mergedType.Historizing = variableType.Historizing;
|
|
mergedType.HistorizingSpecified = true;
|
|
}
|
|
}
|
|
|
|
private static void MergeTypes(
|
|
ObjectTypeDesign mergedType,
|
|
ObjectTypeDesign objectType)
|
|
{
|
|
if (objectType.SupportsEventsSpecified)
|
|
{
|
|
mergedType.SupportsEvents = objectType.SupportsEvents;
|
|
mergedType.SupportsEventsSpecified = true;
|
|
}
|
|
}
|
|
|
|
private NodeDesign CreateMergedInstance(
|
|
XmlQualifiedName rootId,
|
|
string relativePath,
|
|
NodeDesign source)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug("Merging Instance: {Root} {Path} {Source}", rootId.Name, relativePath, source.SymbolicId.Name);
|
|
}
|
|
|
|
var type = source as TypeDesign;
|
|
|
|
if (type != null)
|
|
{
|
|
if (type is ReferenceTypeDesign or DataTypeDesign)
|
|
{
|
|
return type;
|
|
}
|
|
type = MergeTypeHierarchy(type);
|
|
}
|
|
|
|
InstanceDesign mergedInstance = null;
|
|
|
|
if (source is InstanceDesign instance)
|
|
{
|
|
mergedInstance = instance.Copy();
|
|
|
|
if (instance is MethodDesign method)
|
|
|
|
{
|
|
((MethodDesign)mergedInstance).MethodDeclarationNode = method;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (type is VariableTypeDesign variableType)
|
|
{
|
|
mergedInstance = CreateMergedInstance(variableType);
|
|
}
|
|
if (type is ObjectTypeDesign objectType)
|
|
|
|
{
|
|
mergedInstance = CreateMergedInstance(objectType);
|
|
}
|
|
mergedInstance.SymbolicName = rootId;
|
|
mergedInstance.NumericId = source.NumericId;
|
|
mergedInstance.NumericIdSpecified = source.NumericIdSpecified;
|
|
mergedInstance.StringId = source.StringId;
|
|
mergedInstance.BrowseName = rootId.Name;
|
|
mergedInstance.DisplayName.Value = rootId.Name;
|
|
mergedInstance.DisplayName.IsAutogenerated = true;
|
|
mergedInstance.TypeDefinition = source.SymbolicId;
|
|
mergedInstance.TypeDefinitionNode = source as TypeDesign;
|
|
}
|
|
|
|
string instanceId = rootId.Name;
|
|
|
|
if (!string.IsNullOrEmpty(relativePath))
|
|
{
|
|
instanceId = NodeDesign.CreateSymbolicId(instanceId, relativePath);
|
|
}
|
|
|
|
mergedInstance.SymbolicId = new XmlQualifiedName(instanceId, rootId.Namespace);
|
|
|
|
mergedInstance.References = null;
|
|
mergedInstance.IdentifierRequired = true;
|
|
mergedInstance.OveriddenNode = null;
|
|
mergedInstance.Parent = null;
|
|
mergedInstance.Category = source.Category;
|
|
mergedInstance.Purpose = source.Purpose;
|
|
mergedInstance.ReleaseStatus = source.ReleaseStatus;
|
|
mergedInstance.DesignToolOnly = source is InstanceDesign dto && dto.DesignToolOnly;
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug("Created Merged Instance: {Name}", mergedInstance.SymbolicId.Name);
|
|
}
|
|
return mergedInstance;
|
|
}
|
|
|
|
private static ObjectDesign CreateMergedInstance(
|
|
ObjectTypeDesign type)
|
|
{
|
|
var objectd = new ObjectDesign
|
|
{
|
|
Parent = null,
|
|
ReferenceType = null,
|
|
ModellingRule = ModellingRule.Mandatory,
|
|
ModellingRuleSpecified = true,
|
|
DisplayName = new LocalizedText(),
|
|
WriteAccess = 0,
|
|
SupportsEvents = type.SupportsEvents,
|
|
SupportsEventsSpecified = true,
|
|
Category = type.Category,
|
|
Purpose = type.Purpose,
|
|
ReleaseStatus = type.ReleaseStatus,
|
|
RolePermissions = type.RolePermissions,
|
|
AccessRestrictions = type.AccessRestrictions,
|
|
AccessRestrictionsSpecified = type.AccessRestrictionsSpecified,
|
|
DefaultRolePermissions = type.DefaultRolePermissions,
|
|
DefaultAccessRestrictions = type.DefaultAccessRestrictions,
|
|
DefaultAccessRestrictionsSpecified = type.DefaultAccessRestrictionsSpecified
|
|
};
|
|
objectd.WriteAccess = 0;
|
|
|
|
return objectd;
|
|
}
|
|
|
|
private static VariableDesign CreateMergedInstance(
|
|
VariableTypeDesign type)
|
|
{
|
|
var variable = new VariableDesign
|
|
{
|
|
Parent = null,
|
|
ReferenceType = null,
|
|
ModellingRule = ModellingRule.Mandatory,
|
|
ModellingRuleSpecified = true,
|
|
DisplayName = new LocalizedText(),
|
|
WriteAccess = 0,
|
|
DecodedValue = type.DecodedValue,
|
|
DefaultValue = type.DefaultValue,
|
|
DataType = type.DataType,
|
|
DataTypeNode = type.DataTypeNode,
|
|
ValueRank = type.ValueRank,
|
|
ValueRankSpecified = type.ValueRankSpecified,
|
|
ArrayDimensions = type.ArrayDimensions,
|
|
AccessLevel = type.AccessLevel,
|
|
AccessLevelSpecified = type.AccessLevelSpecified,
|
|
MinimumSamplingInterval = type.MinimumSamplingInterval,
|
|
MinimumSamplingIntervalSpecified = type.MinimumSamplingIntervalSpecified,
|
|
Historizing = type.Historizing,
|
|
HistorizingSpecified = type.HistorizingSpecified,
|
|
Category = type.Category,
|
|
Purpose = type.Purpose,
|
|
ReleaseStatus = type.ReleaseStatus,
|
|
RolePermissions = type.RolePermissions,
|
|
AccessRestrictions = type.AccessRestrictions,
|
|
AccessRestrictionsSpecified = type.AccessRestrictionsSpecified,
|
|
DefaultRolePermissions = type.DefaultRolePermissions,
|
|
DefaultAccessRestrictions = type.DefaultAccessRestrictions,
|
|
DefaultAccessRestrictionsSpecified = type.DefaultAccessRestrictionsSpecified
|
|
};
|
|
variable.WriteAccess = 0;
|
|
|
|
return variable;
|
|
}
|
|
|
|
private void UpdateMergedInstance(
|
|
InstanceDesign mergedInstance,
|
|
NodeDesign source)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Updated Merged Instance: {Instance} {Source}",
|
|
mergedInstance.SymbolicId.Name,
|
|
source.SymbolicId.Name);
|
|
}
|
|
|
|
if (source.DisplayName != null && !source.DisplayName.IsAutogenerated)
|
|
|
|
{
|
|
mergedInstance.DisplayName = source.DisplayName;
|
|
}
|
|
if (source.Description != null && !source.Description.IsAutogenerated)
|
|
|
|
{
|
|
mergedInstance.Description = source.Description;
|
|
}
|
|
if (source.RolePermissions != null)
|
|
|
|
{
|
|
mergedInstance.RolePermissions = source.RolePermissions;
|
|
}
|
|
if (source.WriteAccess != 0)
|
|
|
|
{
|
|
mergedInstance.WriteAccess = source.WriteAccess;
|
|
}
|
|
if (source.AccessRestrictionsSpecified)
|
|
{
|
|
mergedInstance.AccessRestrictions = source.AccessRestrictions;
|
|
mergedInstance.AccessRestrictionsSpecified = source.AccessRestrictionsSpecified;
|
|
}
|
|
|
|
if (source is InstanceDesign instance)
|
|
{
|
|
if (source.NumericId != 0 && source.NumericId != mergedInstance.NumericId)
|
|
{
|
|
mergedInstance.NumericId = source.NumericId;
|
|
mergedInstance.NumericIdSpecified = source.NumericIdSpecified;
|
|
}
|
|
|
|
if (source.StringId != null && source.StringId != mergedInstance.StringId)
|
|
{
|
|
mergedInstance.StringId = source.StringId;
|
|
mergedInstance.NumericIdSpecified = source.NumericIdSpecified;
|
|
}
|
|
|
|
if (mergedInstance.SymbolicName != source.SymbolicName)
|
|
{
|
|
mergedInstance.SymbolicName = source.SymbolicName;
|
|
mergedInstance.BrowseName = source.BrowseName;
|
|
mergedInstance.DisplayName = source.DisplayName;
|
|
}
|
|
|
|
UpdateMergedInstance(mergedInstance, instance);
|
|
|
|
if (source is InstanceDesign dto && dto.DesignToolOnly)
|
|
|
|
{
|
|
mergedInstance.DesignToolOnly = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (source is VariableTypeDesign variableType)
|
|
{
|
|
UpdateMergedInstance((VariableDesign)mergedInstance, variableType);
|
|
}
|
|
if (source is ObjectTypeDesign objectType)
|
|
|
|
{
|
|
UpdateMergedInstance((ObjectDesign)mergedInstance, objectType);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void UpdateMergedInstance(
|
|
InstanceDesign mergedInstance,
|
|
InstanceDesign instance)
|
|
{
|
|
mergedInstance.ReferenceType = instance.ReferenceType;
|
|
|
|
if (instance.ModellingRuleSpecified)
|
|
{
|
|
mergedInstance.ModellingRule = instance.ModellingRule;
|
|
mergedInstance.ModellingRuleSpecified = true;
|
|
}
|
|
|
|
if (instance is VariableDesign variable)
|
|
|
|
{
|
|
UpdateMergedInstance((VariableDesign)mergedInstance, variable);
|
|
}
|
|
if (instance is ObjectDesign objectd)
|
|
|
|
{
|
|
UpdateMergedInstance((ObjectDesign)mergedInstance, objectd);
|
|
}
|
|
if (instance is MethodDesign method)
|
|
|
|
{
|
|
UpdateMergedInstance((MethodDesign)mergedInstance, method);
|
|
}
|
|
}
|
|
|
|
private static void UpdateMergedInstance(
|
|
VariableDesign mergedVariable,
|
|
VariableTypeDesign variableType)
|
|
{
|
|
mergedVariable.TypeDefinition = variableType.SymbolicId;
|
|
mergedVariable.TypeDefinitionNode = variableType;
|
|
|
|
if (variableType.DecodedValue != null)
|
|
|
|
{
|
|
mergedVariable.DecodedValue = variableType.DecodedValue;
|
|
}
|
|
if (variableType.DefaultValue != null)
|
|
|
|
{
|
|
mergedVariable.DefaultValue = variableType.DefaultValue;
|
|
}
|
|
if (variableType.DataType != null &&
|
|
variableType.DataType != s_baseDataTypeQn)
|
|
{
|
|
mergedVariable.DataType = variableType.DataType;
|
|
mergedVariable.DataTypeNode = variableType.DataTypeNode;
|
|
}
|
|
|
|
if (variableType.ValueRankSpecified)
|
|
{
|
|
mergedVariable.ValueRank = variableType.ValueRank;
|
|
mergedVariable.ValueRankSpecified = true;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(variableType.ArrayDimensions))
|
|
{
|
|
mergedVariable.ArrayDimensions = variableType.ArrayDimensions;
|
|
}
|
|
|
|
if (variableType.AccessLevelSpecified)
|
|
{
|
|
mergedVariable.AccessLevel = variableType.AccessLevel;
|
|
mergedVariable.AccessLevelSpecified = true;
|
|
}
|
|
|
|
if (variableType.MinimumSamplingIntervalSpecified)
|
|
{
|
|
mergedVariable.MinimumSamplingInterval =
|
|
variableType.MinimumSamplingInterval;
|
|
mergedVariable.MinimumSamplingIntervalSpecified = true;
|
|
}
|
|
|
|
if (variableType.HistorizingSpecified)
|
|
{
|
|
mergedVariable.Historizing = variableType.Historizing;
|
|
mergedVariable.HistorizingSpecified = true;
|
|
}
|
|
|
|
if (variableType.DefaultRolePermissions != null)
|
|
{
|
|
mergedVariable.DefaultRolePermissions =
|
|
variableType.DefaultRolePermissions;
|
|
}
|
|
|
|
if (variableType.DefaultAccessRestrictionsSpecified &&
|
|
!mergedVariable.DefaultAccessRestrictionsSpecified)
|
|
{
|
|
mergedVariable.DefaultAccessRestrictions =
|
|
variableType.DefaultAccessRestrictions;
|
|
mergedVariable.DefaultAccessRestrictionsSpecified = true;
|
|
}
|
|
}
|
|
|
|
private static void UpdateMergedInstance(
|
|
VariableDesign mergedVariable,
|
|
VariableDesign variable)
|
|
{
|
|
if (variable.TypeDefinition != null &&
|
|
variable.TypeDefinition != s_baseDataVariableTypeQn)
|
|
{
|
|
mergedVariable.TypeDefinition = variable.TypeDefinition;
|
|
mergedVariable.TypeDefinitionNode = variable.TypeDefinitionNode;
|
|
}
|
|
|
|
if (variable.DecodedValue != null)
|
|
|
|
{
|
|
mergedVariable.DecodedValue = variable.DecodedValue;
|
|
}
|
|
if (variable.DefaultValue != null)
|
|
|
|
{
|
|
mergedVariable.DefaultValue = variable.DefaultValue;
|
|
}
|
|
if (variable.DataType != null &&
|
|
variable.DataType != s_baseDataTypeQn)
|
|
{
|
|
mergedVariable.DataType = variable.DataType;
|
|
mergedVariable.DataTypeNode = variable.DataTypeNode;
|
|
}
|
|
|
|
if (variable.ValueRankSpecified)
|
|
{
|
|
mergedVariable.ValueRank = variable.ValueRank;
|
|
mergedVariable.ValueRankSpecified = true;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(variable.ArrayDimensions))
|
|
{
|
|
mergedVariable.ArrayDimensions = variable.ArrayDimensions;
|
|
}
|
|
|
|
if (variable.AccessLevelSpecified)
|
|
{
|
|
mergedVariable.AccessLevel = variable.AccessLevel;
|
|
mergedVariable.AccessLevelSpecified = true;
|
|
}
|
|
|
|
if (variable.MinimumSamplingIntervalSpecified)
|
|
{
|
|
mergedVariable.MinimumSamplingInterval = variable.MinimumSamplingInterval;
|
|
mergedVariable.MinimumSamplingIntervalSpecified = true;
|
|
}
|
|
|
|
if (variable.HistorizingSpecified)
|
|
{
|
|
mergedVariable.Historizing = variable.Historizing;
|
|
mergedVariable.HistorizingSpecified = true;
|
|
}
|
|
}
|
|
|
|
private static void UpdateMergedInstance(
|
|
ObjectDesign mergedObject,
|
|
ObjectTypeDesign objectType)
|
|
{
|
|
mergedObject.TypeDefinition = objectType.SymbolicId;
|
|
mergedObject.TypeDefinitionNode = objectType;
|
|
|
|
if (objectType.SupportsEventsSpecified)
|
|
{
|
|
mergedObject.SupportsEvents = objectType.SupportsEvents;
|
|
mergedObject.SupportsEventsSpecified = true;
|
|
}
|
|
|
|
if (objectType.DefaultRolePermissions != null)
|
|
|
|
{
|
|
mergedObject.DefaultRolePermissions = objectType.DefaultRolePermissions;
|
|
}
|
|
if (objectType.DefaultAccessRestrictionsSpecified && !mergedObject.DefaultAccessRestrictionsSpecified)
|
|
{
|
|
mergedObject.DefaultAccessRestrictions = objectType.DefaultAccessRestrictions;
|
|
mergedObject.DefaultAccessRestrictionsSpecified = true;
|
|
}
|
|
}
|
|
|
|
private static void UpdateMergedInstance(
|
|
MethodDesign mergedMethod,
|
|
MethodDesign method)
|
|
{
|
|
if (method.NonExecutableSpecified)
|
|
{
|
|
mergedMethod.NonExecutable = method.NonExecutable;
|
|
mergedMethod.NonExecutableSpecified = true;
|
|
}
|
|
|
|
if (method.DefaultRolePermissions != null)
|
|
|
|
{
|
|
mergedMethod.DefaultRolePermissions = method.DefaultRolePermissions;
|
|
}
|
|
if (method.DefaultAccessRestrictionsSpecified &&
|
|
!mergedMethod.DefaultAccessRestrictionsSpecified)
|
|
{
|
|
mergedMethod.DefaultAccessRestrictions = method.DefaultAccessRestrictions;
|
|
mergedMethod.DefaultAccessRestrictionsSpecified = true;
|
|
}
|
|
}
|
|
|
|
private static void UpdateMergedInstance(
|
|
ObjectDesign mergedObject,
|
|
ObjectDesign objectd)
|
|
{
|
|
if (objectd.TypeDefinition != null &&
|
|
objectd.TypeDefinition != s_baseObjectTypeQn)
|
|
{
|
|
mergedObject.TypeDefinition = objectd.TypeDefinition;
|
|
mergedObject.TypeDefinitionNode = objectd.TypeDefinitionNode;
|
|
}
|
|
|
|
if (objectd.SupportsEventsSpecified)
|
|
{
|
|
mergedObject.SupportsEvents = objectd.SupportsEvents;
|
|
mergedObject.SupportsEventsSpecified = true;
|
|
}
|
|
}
|
|
|
|
private void SetOverriddenNodes(
|
|
TypeDesign type,
|
|
string basePath,
|
|
Dictionary<string, InstanceDesign> nodes,
|
|
int depth,
|
|
bool fromInstance)
|
|
{
|
|
if (type.BaseTypeNode != null)
|
|
{
|
|
SetOverriddenNodes(
|
|
type.BaseTypeNode,
|
|
basePath,
|
|
nodes,
|
|
depth + 1,
|
|
fromInstance);
|
|
}
|
|
|
|
if (type.Children != null && type.Children.Items != null)
|
|
{
|
|
for (int ii = 0; ii < type.Children.Items.Length; ii++)
|
|
{
|
|
InstanceDesign instance = type.Children.Items[ii];
|
|
|
|
if (instance.ModellingRule == ModellingRule.ExposesItsArray)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
string browsePath = GetBrowsePath(basePath, instance);
|
|
|
|
if (instance.ModellingRule is
|
|
ModellingRule.MandatoryPlaceholder or
|
|
ModellingRule.OptionalPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SetOverriddenNodes(instance, browsePath, nodes, depth + 1);
|
|
|
|
if (nodes.TryGetValue(browsePath, out InstanceDesign overriddenInstance))
|
|
{
|
|
bool inPath = false;
|
|
|
|
for (InstanceDesign current = overriddenInstance;
|
|
current != null;
|
|
current = current.OveriddenNode)
|
|
{
|
|
if (current.SymbolicId == instance.SymbolicId)
|
|
{
|
|
inPath = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!inPath)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"OverridingInstance: {Instance} : {Overridden}",
|
|
instance.SymbolicId.Name,
|
|
overriddenInstance.SymbolicId.Name);
|
|
}
|
|
instance.OveriddenNode = overriddenInstance;
|
|
}
|
|
}
|
|
|
|
// special handling for built-in properties.
|
|
var propertyName = new XmlQualifiedName(
|
|
"EnumStrings",
|
|
Ua.Types.Namespaces.OpcUa);
|
|
|
|
if (instance is PropertyDesign &&
|
|
instance.SymbolicName == propertyName)
|
|
{
|
|
instance.OveriddenNode = this.FindNode<VariableDesign>(
|
|
new XmlQualifiedName("EnumStrings", Ua.Types.Namespaces.OpcUa),
|
|
instance.SymbolicId.Name,
|
|
propertyName.Name);
|
|
}
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"IndexingInstance: {Path} : {Instance}",
|
|
browsePath,
|
|
instance.SymbolicId.Name);
|
|
}
|
|
nodes[browsePath] = instance;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SetOverriddenNodes(
|
|
InstanceDesign parent,
|
|
string basePath,
|
|
Dictionary<string, InstanceDesign> nodes,
|
|
int depth)
|
|
{
|
|
if (depth > MaxRecursionDepth)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"{parent.SymbolicId.Name} max recursion exceeded. Check model.");
|
|
}
|
|
|
|
if (parent.TypeDefinitionNode != null)
|
|
{
|
|
SetOverriddenNodes(
|
|
parent.TypeDefinitionNode,
|
|
basePath,
|
|
nodes,
|
|
depth + 1,
|
|
true);
|
|
}
|
|
|
|
if (parent.Children != null && parent.Children.Items != null)
|
|
{
|
|
for (int ii = 0; ii < parent.Children.Items.Length; ii++)
|
|
{
|
|
InstanceDesign instance = parent.Children.Items[ii];
|
|
|
|
if (instance.ModellingRule == ModellingRule.ExposesItsArray)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
string browsePath = GetBrowsePath(basePath, instance);
|
|
|
|
if (instance.ModellingRule is
|
|
ModellingRule.MandatoryPlaceholder or
|
|
ModellingRule.OptionalPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SetOverriddenNodes(instance, browsePath, nodes, depth + 1);
|
|
|
|
if (nodes.TryGetValue(browsePath, out InstanceDesign overriddenInstance))
|
|
{
|
|
bool inPath = false;
|
|
|
|
for (InstanceDesign current = overriddenInstance;
|
|
current != null;
|
|
current = current.OveriddenNode)
|
|
{
|
|
if (current.SymbolicId == instance.SymbolicId)
|
|
{
|
|
inPath = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!inPath)
|
|
|
|
{
|
|
instance.OveriddenNode = overriddenInstance;
|
|
}
|
|
}
|
|
|
|
nodes[browsePath] = instance;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collects all of children for a node.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
private void SetOverriddenNodes(NodeDesign node, int depth)
|
|
{
|
|
if (node is ReferenceTypeDesign or DataTypeDesign)
|
|
{
|
|
return;
|
|
}
|
|
if (depth > MaxRecursionDepth)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"{node.SymbolicId.Name} max recursion exceeded. Check model.");
|
|
}
|
|
|
|
var nodes = new Dictionary<string, InstanceDesign>();
|
|
switch (node)
|
|
{
|
|
case TypeDesign type:
|
|
SetOverriddenNodes(type, string.Empty, nodes, depth + 1, false);
|
|
break;
|
|
case InstanceDesign instance:
|
|
SetOverriddenNodes(instance, string.Empty, nodes, depth + 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void TranslateReferences(
|
|
string currentPath,
|
|
NodeDesign source,
|
|
List<HierarchyReference> references,
|
|
bool suppressInverseHierarchicalAtTypeLevel,
|
|
bool inherited)
|
|
{
|
|
if (source.References == null || source.References.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
for (int ii = 0; ii < source.References.Length; ii++)
|
|
{
|
|
if (source.References[ii].ReferenceType ==
|
|
new XmlQualifiedName("HasModelParent", Ua.Types.Namespaces.OpcUa))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// suppress inhierited non-hierarchial references.
|
|
if (inherited &&
|
|
m_nodes.TryGetValue(source.References[ii].ReferenceType, out NodeDesign target))
|
|
{
|
|
var referenceType = target as ReferenceTypeDesign;
|
|
|
|
bool found = false;
|
|
|
|
while (referenceType != null)
|
|
{
|
|
if (referenceType.SymbolicName ==
|
|
new XmlQualifiedName("NonHierarchicalReferences", Ua.Types.Namespaces.OpcUa))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
referenceType = referenceType.BaseTypeNode as ReferenceTypeDesign;
|
|
}
|
|
|
|
if (found)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (suppressInverseHierarchicalAtTypeLevel &&
|
|
source.References[ii].IsInverse &&
|
|
source.References[ii].ReferenceType ==
|
|
new XmlQualifiedName("Organizes", Ua.Types.Namespaces.OpcUa))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
HierarchyReference reference = TranslateReference(
|
|
currentPath,
|
|
source.SymbolicId,
|
|
source.References[ii]);
|
|
|
|
references.Add(reference);
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Translated Reference: {Source} => {Reference} => {Target}",
|
|
string.IsNullOrEmpty(reference.SourcePath) ?
|
|
source.SymbolicId.Name :
|
|
reference.SourcePath,
|
|
reference.ReferenceType.Name,
|
|
reference.TargetId != null ?
|
|
reference.TargetId.Name :
|
|
reference.TargetPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static HierarchyReference TranslateReference(
|
|
string currentPath,
|
|
XmlQualifiedName sourceId,
|
|
Reference reference)
|
|
{
|
|
currentPath ??= string.Empty;
|
|
|
|
var mergedReference = new HierarchyReference
|
|
{
|
|
SourcePath = currentPath,
|
|
ReferenceType = reference.ReferenceType,
|
|
IsInverse = reference.IsInverse,
|
|
TargetId = reference.TargetId
|
|
};
|
|
|
|
if (reference.TargetId == null ||
|
|
sourceId.Namespace != reference.TargetId.Namespace ||
|
|
reference.ReferenceType == new XmlQualifiedName("HasEncoding", Ua.Types.Namespaces.OpcUa))
|
|
{
|
|
return mergedReference;
|
|
}
|
|
|
|
string[] currentPathParts = currentPath.Split(
|
|
NodeDesign.PathChars,
|
|
StringSplitOptions.RemoveEmptyEntries);
|
|
string[] sourceIdParts = sourceId.Name.Split(
|
|
NodeDesign.PathChars,
|
|
StringSplitOptions.RemoveEmptyEntries);
|
|
string[] targetIdParts = reference.TargetId.Name.Split(
|
|
NodeDesign.PathChars,
|
|
StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// find the common root in the type declaration.
|
|
string[] targetPath = null;
|
|
string[] sourcePath = null;
|
|
|
|
if (sourceIdParts.Length == 0 ||
|
|
targetIdParts.Length == 0 ||
|
|
targetIdParts[0] != sourceIdParts[0])
|
|
{
|
|
return mergedReference;
|
|
}
|
|
|
|
for (int ii = 0; ii < sourceIdParts.Length; ii++)
|
|
{
|
|
if (ii >= targetIdParts.Length)
|
|
{
|
|
sourcePath = new string[sourceIdParts.Length - ii];
|
|
Array.Copy(sourceIdParts, ii, sourcePath, 0, sourcePath.Length);
|
|
targetPath = [];
|
|
break;
|
|
}
|
|
|
|
if (targetIdParts[ii] != sourceIdParts[ii])
|
|
{
|
|
sourcePath = new string[sourceIdParts.Length - ii];
|
|
Array.Copy(sourceIdParts, ii, sourcePath, 0, sourcePath.Length);
|
|
targetPath = new string[targetIdParts.Length - ii];
|
|
Array.Copy(targetIdParts, ii, targetPath, 0, targetPath.Length);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// no common root.
|
|
if (sourcePath == null)
|
|
{
|
|
sourcePath = [];
|
|
targetPath = new string[targetIdParts.Length - sourceIdParts.Length];
|
|
Array.Copy(targetIdParts, sourceIdParts.Length, targetPath, 0, targetPath.Length);
|
|
}
|
|
|
|
// find the new root.
|
|
string[] targetRoot = null;
|
|
|
|
for (int ii = 1; ii <= sourcePath.Length - 1; ii++)
|
|
{
|
|
if (ii > currentPathParts.Length)
|
|
{
|
|
return mergedReference;
|
|
}
|
|
if (currentPathParts[^ii] != sourcePath[^ii])
|
|
{
|
|
targetRoot = new string[currentPathParts.Length - ii];
|
|
Array.Copy(currentPathParts, 0, targetRoot, 0, targetRoot.Length);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// no common root.
|
|
if (targetRoot == null)
|
|
{
|
|
if (currentPathParts.Length < sourcePath.Length)
|
|
{
|
|
return mergedReference;
|
|
}
|
|
targetRoot = new string[currentPathParts.Length - sourcePath.Length];
|
|
Array.Copy(currentPathParts, 0, targetRoot, 0, targetRoot.Length);
|
|
}
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
for (int ii = 0; ii < targetRoot.Length; ii++)
|
|
{
|
|
if (builder.Length > 0)
|
|
{
|
|
builder.Append(NodeDesign.PathChar);
|
|
}
|
|
builder.Append(targetRoot[ii]);
|
|
}
|
|
|
|
if (targetPath != null)
|
|
{
|
|
for (int ii = 0; ii < targetPath.Length; ii++)
|
|
{
|
|
if (builder.Length > 0)
|
|
{
|
|
builder.Append(NodeDesign.PathChar);
|
|
}
|
|
builder.Append(targetPath[ii]);
|
|
}
|
|
}
|
|
|
|
mergedReference.TargetId = null;
|
|
mergedReference.TargetPath = builder.ToString();
|
|
|
|
return mergedReference;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collects all of children for a type.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
private void BuildInstanceHierarchy(
|
|
TypeDesign type,
|
|
string basePath,
|
|
List<HierarchyNode> nodes,
|
|
List<HierarchyReference> references,
|
|
bool suppressInverseHierarchicalAtTypeLevel,
|
|
bool inherited,
|
|
int depth)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"BuildHierarchy for Type: {Type} : {Path}",
|
|
type.SymbolicId.Name,
|
|
basePath);
|
|
}
|
|
|
|
if (depth > MaxRecursionDepth)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"{basePath} max recursion exceeded. Check model.");
|
|
}
|
|
|
|
if (type.BaseTypeNode != null &&
|
|
type is VariableTypeDesign or ObjectTypeDesign)
|
|
{
|
|
BuildInstanceHierarchy(
|
|
type.BaseTypeNode,
|
|
basePath,
|
|
nodes,
|
|
references,
|
|
true,
|
|
true,
|
|
depth + 1);
|
|
}
|
|
|
|
TranslateReferences(
|
|
basePath,
|
|
type,
|
|
references,
|
|
suppressInverseHierarchicalAtTypeLevel,
|
|
inherited);
|
|
|
|
if (type.Children != null && type.Children.Items != null)
|
|
{
|
|
for (int ii = 0; ii < type.Children.Items.Length; ii++)
|
|
{
|
|
InstanceDesign instance = type.Children.Items[ii];
|
|
|
|
string browsePath = GetBrowsePath(basePath, instance);
|
|
|
|
if (!string.IsNullOrEmpty(basePath))
|
|
{
|
|
if (instance.ModellingRule is
|
|
ModellingRule.None or
|
|
ModellingRule.ExposesItsArray or
|
|
ModellingRule.OptionalPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (instance.ModellingRule == ModellingRule.MandatoryPlaceholder)
|
|
{
|
|
var reference = new HierarchyReference
|
|
{
|
|
SourcePath = basePath,
|
|
ReferenceType = instance.ReferenceType,
|
|
IsInverse = false,
|
|
TargetId = instance.SymbolicId
|
|
};
|
|
|
|
references.Add(reference);
|
|
|
|
//if (instance.SymbolicId.Namespace != DefaultNamespace)
|
|
//{
|
|
// m_logger.LogDebug("TypeDesign Placeholder: {Source} => {Target}",
|
|
// reference.SourcePath, reference.TargetId);
|
|
//}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var child = new HierarchyNode
|
|
{
|
|
RelativePath = browsePath,
|
|
Instance = instance,
|
|
Inherited = inherited
|
|
};
|
|
|
|
nodes.Add(child);
|
|
BuildInstanceHierarchy(
|
|
instance,
|
|
browsePath,
|
|
nodes,
|
|
references,
|
|
inherited,
|
|
depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collects all of children for an instance.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
private void BuildInstanceHierarchy(
|
|
InstanceDesign parent,
|
|
string basePath,
|
|
List<HierarchyNode> nodes,
|
|
List<HierarchyReference> references,
|
|
bool inherited,
|
|
int depth)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"BuildHierarchy for Instance: {Name} : {Path}",
|
|
parent.SymbolicId.Name,
|
|
basePath);
|
|
}
|
|
|
|
if (depth > MaxRecursionDepth)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"{basePath} max recursion exceeded. Check model.");
|
|
}
|
|
|
|
if (parent.TypeDefinitionNode != null)
|
|
{
|
|
BuildInstanceHierarchy(
|
|
parent.TypeDefinitionNode,
|
|
basePath,
|
|
nodes,
|
|
references,
|
|
true,
|
|
inherited,
|
|
depth + 1);
|
|
}
|
|
|
|
if (parent.TypeDefinition != null && parent is MethodDesign)
|
|
{
|
|
MethodDesign methodType = this.FindNode<MethodDesign>(
|
|
parent.TypeDefinition,
|
|
parent.SymbolicId.Name,
|
|
"MethodType");
|
|
|
|
if (methodType != null)
|
|
{
|
|
BuildInstanceHierarchy(
|
|
methodType,
|
|
basePath,
|
|
nodes,
|
|
references,
|
|
inherited,
|
|
depth + 1);
|
|
}
|
|
}
|
|
|
|
TranslateReferences(basePath, parent, references, false, false);
|
|
|
|
if (parent.Children != null && parent.Children.Items != null)
|
|
{
|
|
bool isTypeDefinition = parent.Parent is TypeDesign;
|
|
|
|
for (int ii = 0; ii < parent.Children.Items.Length; ii++)
|
|
{
|
|
InstanceDesign instance = parent.Children.Items[ii];
|
|
|
|
string browsePath = GetBrowsePath(basePath, instance);
|
|
|
|
if (inherited && !string.IsNullOrEmpty(basePath))
|
|
{
|
|
if (instance.ModellingRule is ModellingRule.None or
|
|
ModellingRule.ExposesItsArray or
|
|
ModellingRule.OptionalPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (instance.ModellingRule == ModellingRule.MandatoryPlaceholder)
|
|
{
|
|
HierarchyReference reference = references
|
|
.FirstOrDefault(x => x.SourcePath == browsePath);
|
|
|
|
if (reference == null)
|
|
{
|
|
reference = new HierarchyReference();
|
|
references.Add(reference);
|
|
}
|
|
|
|
reference.SourcePath = basePath;
|
|
reference.ReferenceType = instance.ReferenceType;
|
|
reference.IsInverse = false;
|
|
reference.TargetId = instance.SymbolicId;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var child = new HierarchyNode
|
|
{
|
|
RelativePath = browsePath,
|
|
Instance = instance,
|
|
Inherited = inherited
|
|
};
|
|
|
|
if (isTypeDefinition && parent is not MethodDesign)
|
|
{
|
|
if (instance.OveriddenNode == null)
|
|
{
|
|
child.AdHocInstance = true;
|
|
}
|
|
if (instance.ModellingRule == ModellingRule.None &&
|
|
instance is VariableDesign &&
|
|
instance.OveriddenNode != null)
|
|
{
|
|
child.StaticValue = true;
|
|
}
|
|
}
|
|
|
|
if (instance.ModellingRule
|
|
is ModellingRule.OptionalPlaceholder
|
|
or ModellingRule.MandatoryPlaceholder &&
|
|
depth > 3)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
nodes.Add(child);
|
|
|
|
BuildInstanceHierarchy(
|
|
instance,
|
|
browsePath,
|
|
nodes,
|
|
references,
|
|
inherited,
|
|
depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collects all of children for a node.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
private void BuildInstanceHierarchy(
|
|
NodeDesign node,
|
|
List<HierarchyNode> nodes,
|
|
List<HierarchyReference> references,
|
|
int depth)
|
|
{
|
|
if (depth > MaxRecursionDepth)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"{node.SymbolicId.Name} max recursion exceeded. Check model.");
|
|
}
|
|
|
|
switch (node)
|
|
{
|
|
case TypeDesign type:
|
|
BuildInstanceHierarchy(
|
|
type,
|
|
string.Empty,
|
|
nodes,
|
|
references,
|
|
false,
|
|
false,
|
|
depth + 1);
|
|
break;
|
|
case InstanceDesign instance:
|
|
BuildInstanceHierarchy(
|
|
instance,
|
|
string.Empty,
|
|
nodes,
|
|
references,
|
|
false,
|
|
depth + 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private Hierarchy BuildInstanceHierarchy(NodeDesign root, int depth)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Building InstanceHierarchy for {Name}",
|
|
root.SymbolicId.Name);
|
|
}
|
|
if (depth > MaxRecursionDepth)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"{root.SymbolicId.Name} max recursion exceeded. Check model.");
|
|
}
|
|
|
|
bool designToolOnly = root is InstanceDesign dto && dto.DesignToolOnly;
|
|
|
|
SetOverriddenNodes(root, 0);
|
|
|
|
// collect all of the nodes that define the hierachy.
|
|
var nodes = new List<HierarchyNode>();
|
|
var references = new List<HierarchyReference>();
|
|
|
|
if (!designToolOnly)
|
|
|
|
{
|
|
BuildInstanceHierarchy(root, nodes, references, depth + 1);
|
|
}
|
|
var hierarchy = new Hierarchy
|
|
{
|
|
References = references
|
|
};
|
|
|
|
XmlQualifiedName rootId = root.SymbolicId;
|
|
|
|
// add root node.
|
|
var instance = root as InstanceDesign;
|
|
|
|
if (instance == null)
|
|
{
|
|
rootId = new XmlQualifiedName(
|
|
"InstanceOf" + root.SymbolicId.Name,
|
|
root.SymbolicId.Namespace);
|
|
}
|
|
|
|
var rootNode = new HierarchyNode
|
|
{
|
|
RelativePath = string.Empty
|
|
};
|
|
|
|
if (instance == null || instance.TypeDefinitionNode == null)
|
|
{
|
|
rootNode.Instance = CreateMergedInstance(
|
|
rootId,
|
|
string.Empty,
|
|
root);
|
|
}
|
|
else
|
|
{
|
|
rootNode.Instance = CreateMergedInstance(
|
|
rootId,
|
|
string.Empty,
|
|
instance.TypeDefinitionNode);
|
|
|
|
if (root.SymbolicName == rootNode.Instance.SymbolicName)
|
|
{
|
|
rootNode.Instance.BrowseName = root.BrowseName;
|
|
rootNode.Instance.DisplayName = root.DisplayName;
|
|
}
|
|
|
|
UpdateMergedInstance((InstanceDesign)rootNode.Instance, root);
|
|
}
|
|
|
|
if (designToolOnly)
|
|
|
|
{
|
|
rootNode.Instance.Children = new ListOfChildren();
|
|
}
|
|
rootNode.ExplicitlyDefined = false;
|
|
|
|
hierarchy.Nodes.Add(string.Empty, rootNode);
|
|
hierarchy.NodeList.Add(rootNode);
|
|
|
|
// build instance hierachy.
|
|
if (!designToolOnly)
|
|
{
|
|
for (int ii = 0; ii < nodes.Count; ii++)
|
|
{
|
|
HierarchyNode node = nodes[ii];
|
|
|
|
bool explicitlyDefined = false;
|
|
|
|
for (NodeDesign parent = node.Instance; parent != null; parent = parent.Parent)
|
|
{
|
|
if (parent.SymbolicId == root.SymbolicId)
|
|
{
|
|
explicitlyDefined = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hierarchy.Nodes.TryGetValue(node.RelativePath, out HierarchyNode mergedNode))
|
|
{
|
|
mergedNode = new HierarchyNode
|
|
{
|
|
RelativePath = node.RelativePath,
|
|
Instance = CreateMergedInstance(
|
|
root.SymbolicId,
|
|
node.RelativePath,
|
|
node.Instance),
|
|
ExplicitlyDefined = false,
|
|
Inherited = node.Inherited,
|
|
AdHocInstance = node.AdHocInstance,
|
|
TypeDefinitionModellingRule =
|
|
(node.Instance as InstanceDesign)?.ModellingRule
|
|
};
|
|
|
|
hierarchy.Nodes.Add(node.RelativePath, mergedNode);
|
|
hierarchy.NodeList.Add(mergedNode);
|
|
}
|
|
else
|
|
{
|
|
UpdateMergedInstance((InstanceDesign)mergedNode.Instance, node.Instance);
|
|
mergedNode.StaticValue = node.StaticValue;
|
|
|
|
// Keep TypeDefinitionModellingRule current through
|
|
// type-hierarchy merges (base → derived) so that the
|
|
// effective type-definition rule is captured. Freeze
|
|
// once an explicit instance override is encountered.
|
|
if (!explicitlyDefined &&
|
|
node.Instance is InstanceDesign typeDefOverride &&
|
|
typeDefOverride.ModellingRuleSpecified)
|
|
{
|
|
mergedNode.TypeDefinitionModellingRule =
|
|
typeDefOverride.ModellingRule;
|
|
}
|
|
}
|
|
|
|
if (explicitlyDefined && node.Instance.Extensions != null)
|
|
|
|
{
|
|
mergedNode.Instance.Extensions = node.Instance.Extensions;
|
|
}
|
|
mergedNode.OverriddenNodes ??= [];
|
|
mergedNode.OverriddenNodes.Add(node.Instance);
|
|
|
|
if (explicitlyDefined)
|
|
|
|
{
|
|
mergedNode.ExplicitlyDefined = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return hierarchy;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load model design xml or json
|
|
/// </summary>
|
|
/// <param name="fileToLoad"></param>
|
|
/// <returns></returns>
|
|
private ModelDesign LoadModelDesign(string fileToLoad)
|
|
{
|
|
return Load<ModelDesign>(fileToLoad);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allocates identifiers
|
|
/// </summary>
|
|
private sealed class IdAllocator
|
|
{
|
|
/// <summary>
|
|
/// Allocate identifiers
|
|
/// </summary>
|
|
/// <param name="startId"></param>
|
|
public IdAllocator(uint startId)
|
|
{
|
|
m_lastId = startId;
|
|
m_lastBuiltInId = 15000;
|
|
m_lastImplicitlyDefined = 1000000;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find unused identifier
|
|
/// </summary>
|
|
public uint FindUnusedId(string ns, bool isImplicitlyDefined = false)
|
|
{
|
|
if (ns == Ua.Types.Namespaces.OpcUa)
|
|
{
|
|
return FindUnusedId(ref m_lastBuiltInId);
|
|
}
|
|
else if (isImplicitlyDefined)
|
|
{
|
|
return FindUnusedId(ref m_lastImplicitlyDefined);
|
|
}
|
|
return FindUnusedId(ref m_lastId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add identifier
|
|
/// </summary>
|
|
public void Add(uint value)
|
|
{
|
|
m_identifiers.Add(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find an unused identifier
|
|
/// </summary>
|
|
private uint FindUnusedId(ref uint lastId)
|
|
{
|
|
uint id = lastId;
|
|
while (m_identifiers.Contains(id))
|
|
{
|
|
id++;
|
|
}
|
|
lastId = id;
|
|
m_identifiers.Add(id);
|
|
return id;
|
|
}
|
|
|
|
private uint m_lastId;
|
|
private uint m_lastBuiltInId;
|
|
private uint m_lastImplicitlyDefined;
|
|
private readonly HashSet<uint> m_identifiers = [];
|
|
}
|
|
|
|
private static readonly XmlQualifiedName s_baseDataTypeQn =
|
|
new("BaseDataType", Ua.Types.Namespaces.OpcUa);
|
|
|
|
/// <summary>
|
|
/// Maps a session service name to its OPC UA Part 4 sub-category.
|
|
/// </summary>
|
|
private static ServiceCategory GetSessionSubCategory(string serviceName)
|
|
{
|
|
return serviceName switch
|
|
{
|
|
"Read" or "Write" or "HistoryRead" or "HistoryUpdate"
|
|
=> ServiceCategory.Attribute,
|
|
|
|
"Browse" or "BrowseNext" or "TranslateBrowsePathsToNodeIds"
|
|
or "RegisterNodes" or "UnregisterNodes"
|
|
=> ServiceCategory.View,
|
|
|
|
"Call"
|
|
=> ServiceCategory.Method,
|
|
|
|
"CreateMonitoredItems" or "ModifyMonitoredItems" or "SetMonitoringMode"
|
|
or "SetTriggering" or "DeleteMonitoredItems"
|
|
=> ServiceCategory.MonitoredItem,
|
|
|
|
"CreateSubscription" or "ModifySubscription" or "SetPublishingMode"
|
|
or "Publish" or "Republish" or "TransferSubscriptions" or "DeleteSubscriptions"
|
|
=> ServiceCategory.Subscription,
|
|
|
|
"AddNodes" or "AddReferences" or "DeleteNodes" or "DeleteReferences"
|
|
=> ServiceCategory.NodeManagement,
|
|
|
|
"QueryFirst" or "QueryNext"
|
|
=> ServiceCategory.Query,
|
|
|
|
// Session lifecycle: CreateSession, ActivateSession, CloseSession, Cancel
|
|
_ => ServiceCategory.Session
|
|
};
|
|
}
|
|
|
|
private static readonly XmlQualifiedName s_structureQn =
|
|
new("Structure", Ua.Types.Namespaces.OpcUa);
|
|
|
|
private static readonly XmlQualifiedName s_byteStringQn =
|
|
new("ByteString", Ua.Types.Namespaces.OpcUa);
|
|
|
|
private static readonly XmlQualifiedName s_baseObjectTypeQn =
|
|
new("BaseObjectType", Ua.Types.Namespaces.OpcUa);
|
|
|
|
private static readonly XmlQualifiedName s_baseDataVariableTypeQn =
|
|
new("BaseDataVariableType", Ua.Types.Namespaces.OpcUa);
|
|
|
|
private static readonly XmlQualifiedName s_referencesQn =
|
|
new("References", Ua.Types.Namespaces.OpcUa);
|
|
|
|
private static readonly XmlQualifiedName s_enumerationQn =
|
|
new("Enumeration", Ua.Types.Namespaces.OpcUa);
|
|
|
|
private static readonly XmlQualifiedName s_unionQn =
|
|
new("Union", Ua.Types.Namespaces.OpcUa);
|
|
|
|
private uint m_startId;
|
|
private readonly SpecificationVersion m_standardVersion;
|
|
private readonly ITelemetryContext m_telemetry;
|
|
private readonly ILogger m_logger;
|
|
private Dictionary<XmlQualifiedName, NodeDesign> m_nodes;
|
|
private Dictionary<string, string[]> m_namespaceTables;
|
|
private Dictionary<NodeId, NodeDesign> m_nodesByNodeId;
|
|
private Dictionary<uint, NodeDesign> m_identifiers;
|
|
private Dictionary<XmlQualifiedName, string> m_browseNames = [];
|
|
private readonly ServiceMessageContext m_context;
|
|
private readonly IReadOnlyList<string> m_exclusions;
|
|
private readonly Dictionary<XmlQualifiedName, NodeId> m_symbolicIdToNodeId = [];
|
|
private ModelDesign m_dictionary;
|
|
private Dictionary<string, ModelTableEntry> m_dependencies = [];
|
|
private readonly Dictionary<string, RolePermissionSet> m_defaultRolePermissions = [];
|
|
private readonly Dictionary<string, AccessRestrictions?> m_defaultAccessRestrictions = [];
|
|
private Dictionary<string, string> m_designFilePaths = [];
|
|
|
|
// ──────────────────────────────────────────────────────────────────
|
|
// Dependency import — formerly the SnapshotImport partial.
|
|
// Lets a downstream consumer's source generator ingest the type
|
|
// table from a referenced assembly's
|
|
// [assembly: ModelDependencyAttribute(..., payload)] without the
|
|
// consumer having to re-add the upstream NodeSet2/ModelDesign XML
|
|
// to AdditionalFiles.
|
|
// ──────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Imports a model dependency payload into the validator's node
|
|
/// table. Safe to call multiple times. Stored payloads are
|
|
/// re-applied automatically inside <c>ValidateModel</c> after the
|
|
/// validator's internal tables are reset, so the dependency types
|
|
/// are visible to the dependency-loading pass.
|
|
/// </summary>
|
|
/// <param name="dependency">The deserialised dependency payload.</param>
|
|
/// <param name="prefix">The C# prefix the producing assembly uses
|
|
/// for the dependency's model (so the synthetic Namespace entry
|
|
/// matches what downstream emitters expect to see).</param>
|
|
/// <param name="name">The C# identifier the producing assembly used
|
|
/// inside its <c>Namespaces</c> class for the dependency's model
|
|
/// (e.g. <c>"OpcUaDi"</c>).</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="dependency"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public void ImportDependency(
|
|
ModelDependencyV1 dependency,
|
|
string prefix,
|
|
string name)
|
|
{
|
|
if (dependency == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dependency));
|
|
}
|
|
if (string.IsNullOrEmpty(dependency.ModelUri))
|
|
{
|
|
throw new ArgumentException(
|
|
"Dependency payload has no ModelUri.",
|
|
nameof(dependency));
|
|
}
|
|
m_pendingDependencies ??= [];
|
|
m_pendingDependencies.Add(new PendingDependency(dependency, prefix, name));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called from inside <c>ValidateModel</c> right after the
|
|
/// node-table reset and the built-in OpcUa model load, so all
|
|
/// stored dependency entries are registered before dependency
|
|
/// design files are processed.
|
|
/// </summary>
|
|
internal void ApplyPendingDependencies()
|
|
{
|
|
if (m_pendingDependencies == null || m_pendingDependencies.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
EnsureDependencyTablesInitialised();
|
|
foreach (PendingDependency pending in m_pendingDependencies)
|
|
{
|
|
ApplyDependency(pending.Dependency);
|
|
}
|
|
// Cross-dependency references can now resolve.
|
|
LinkDependencyChildren();
|
|
}
|
|
|
|
private void ApplyDependency(ModelDependencyV1 dependency)
|
|
{
|
|
// Mark the dependency's namespace as "resolved without a backing
|
|
// file" so dependency-missing errors do not fire.
|
|
m_designFilePaths[dependency.ModelUri] = string.Empty;
|
|
|
|
// Allocate or recover a namespace index for the dependency model.
|
|
int nsIndex = m_context.NamespaceUris.GetIndexOrAppend(dependency.ModelUri);
|
|
|
|
foreach (DependencyNode entry in dependency.Nodes)
|
|
{
|
|
TypeDesign design = MaterialiseDependencyNode(entry);
|
|
if (design == null)
|
|
{
|
|
continue;
|
|
}
|
|
XmlQualifiedName symbolicId = design.SymbolicId;
|
|
if (symbolicId == null || string.IsNullOrEmpty(symbolicId.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Do not overwrite an entry that the validator already loaded
|
|
// from its own AdditionalFiles — the explicit AdditionalFile
|
|
// always wins.
|
|
if (m_nodes.ContainsKey(symbolicId))
|
|
{
|
|
continue;
|
|
}
|
|
m_nodes[symbolicId] = design;
|
|
|
|
if (entry.NumericId != 0)
|
|
{
|
|
var nodeId = new NodeId(entry.NumericId, (ushort)nsIndex);
|
|
if (!m_nodesByNodeId.ContainsKey(nodeId))
|
|
{
|
|
m_nodesByNodeId[nodeId] = design;
|
|
}
|
|
m_symbolicIdToNodeId[symbolicId] = nodeId;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the dependency-targeted dictionaries exist. The full
|
|
/// validation path creates them as part of regular loading;
|
|
/// when <see cref="ImportDependency"/> is called before validation
|
|
/// the tables must be available so the dependency is not lost.
|
|
/// </summary>
|
|
private void EnsureDependencyTablesInitialised()
|
|
{
|
|
m_nodes ??= [];
|
|
m_nodesByNodeId ??= [];
|
|
m_identifiers ??= [];
|
|
m_namespaceTables ??= [];
|
|
m_designFilePaths ??= [];
|
|
}
|
|
|
|
private static TypeDesign MaterialiseDependencyNode(DependencyNode entry)
|
|
{
|
|
var symbolicId = new XmlQualifiedName(
|
|
entry.SymbolicName ?? string.Empty,
|
|
entry.SymbolicNamespace ?? string.Empty);
|
|
|
|
XmlQualifiedName baseType = null;
|
|
if (!string.IsNullOrEmpty(entry.BaseTypeName))
|
|
{
|
|
baseType = new XmlQualifiedName(
|
|
entry.BaseTypeName,
|
|
entry.BaseTypeNamespace ?? string.Empty);
|
|
}
|
|
|
|
TypeDesign design = entry.Kind switch
|
|
{
|
|
DependencyNodeKind.ObjectType => new ObjectTypeDesign(),
|
|
DependencyNodeKind.VariableType => new VariableTypeDesign(),
|
|
DependencyNodeKind.ReferenceType => new ReferenceTypeDesign(),
|
|
DependencyNodeKind.DataType => new DataTypeDesign(),
|
|
_ => null
|
|
};
|
|
if (design == null)
|
|
{
|
|
return null;
|
|
}
|
|
design.SymbolicName = symbolicId;
|
|
design.SymbolicId = symbolicId;
|
|
design.BrowseName = entry.SymbolicName ?? string.Empty;
|
|
design.DisplayName = new LocalizedText { Value = entry.SymbolicName ?? string.Empty };
|
|
design.ClassName = string.IsNullOrEmpty(entry.ClassName)
|
|
? entry.SymbolicName ?? string.Empty
|
|
: entry.ClassName;
|
|
design.BaseType = baseType;
|
|
design.IsAbstract = entry.IsAbstract;
|
|
if (entry.NumericId != 0)
|
|
{
|
|
design.NumericId = entry.NumericId;
|
|
design.NumericIdSpecified = true;
|
|
}
|
|
if (!string.IsNullOrEmpty(entry.StringId))
|
|
{
|
|
design.StringId = entry.StringId;
|
|
}
|
|
|
|
if (design is DataTypeDesign dt && entry.Fields != null && entry.Fields.Count > 0)
|
|
{
|
|
var parameters = new Parameter[entry.Fields.Count];
|
|
for (int i = 0; i < entry.Fields.Count; i++)
|
|
{
|
|
DependencyDataField f = entry.Fields[i];
|
|
parameters[i] = new Parameter
|
|
{
|
|
Name = f.Name,
|
|
DataType = new XmlQualifiedName(
|
|
f.DataTypeName ?? string.Empty,
|
|
f.DataTypeNamespace ?? string.Empty),
|
|
ValueRank = (ValueRank)f.ValueRank,
|
|
Description = new LocalizedText()
|
|
};
|
|
}
|
|
dt.Fields = parameters;
|
|
dt.HasFields = true;
|
|
}
|
|
else if (entry.Children != null && entry.Children.Count > 0)
|
|
{
|
|
// Reconstruct Children so the consumer's SetOverriddenNodes
|
|
// walk (which descends through BaseTypeNode → Children.Items)
|
|
// can recognise the upstream's inherited members and properly
|
|
// set OveriddenNode on downstream re-declarations.
|
|
// TypeDefinitionNode + DataTypeNode references are resolved
|
|
// later by LinkDependencyChildren() after all dependencies
|
|
// are applied.
|
|
var instanceItems = new InstanceDesign[entry.Children.Count];
|
|
for (int i = 0; i < entry.Children.Count; i++)
|
|
{
|
|
instanceItems[i] = MaterialiseDependencyChild(symbolicId, entry.Children[i]);
|
|
}
|
|
design.Children = new ListOfChildren { Items = instanceItems };
|
|
design.HasChildren = true;
|
|
}
|
|
|
|
// Mark as an external/upstream declaration so consumer
|
|
// generators don't try to re-emit this type locally.
|
|
design.IsDeclaration = true;
|
|
|
|
return design;
|
|
}
|
|
|
|
private static InstanceDesign MaterialiseDependencyChild(
|
|
XmlQualifiedName parentSymbolicId,
|
|
DependencyChild c)
|
|
{
|
|
InstanceDesign instance = c.InstanceKind switch
|
|
{
|
|
3 => new PropertyDesign(),
|
|
2 => new VariableDesign(),
|
|
4 => new MethodDesign(),
|
|
_ => new ObjectDesign()
|
|
};
|
|
instance.BrowseName = c.BrowseName ?? string.Empty;
|
|
var childSymbolicId = new XmlQualifiedName(
|
|
string.IsNullOrEmpty(c.SymbolicName) ? c.BrowseName : c.SymbolicName,
|
|
parentSymbolicId.Namespace);
|
|
instance.SymbolicName = childSymbolicId;
|
|
instance.SymbolicId = new XmlQualifiedName(
|
|
NodeDesign.CreateSymbolicId(
|
|
parentSymbolicId.Name,
|
|
childSymbolicId.Name),
|
|
parentSymbolicId.Namespace);
|
|
instance.ModellingRule = c.ModellingRule switch
|
|
{
|
|
1 => ModellingRule.Mandatory,
|
|
2 => ModellingRule.Optional,
|
|
3 => ModellingRule.OptionalPlaceholder,
|
|
4 => ModellingRule.MandatoryPlaceholder,
|
|
5 => ModellingRule.ExposesItsArray,
|
|
_ => ModellingRule.None
|
|
};
|
|
instance.ModellingRuleSpecified = c.ModellingRule != 0;
|
|
instance.DisplayName = new LocalizedText
|
|
{
|
|
Value = c.BrowseName ?? string.Empty,
|
|
IsAutogenerated = true
|
|
};
|
|
if (!string.IsNullOrEmpty(c.TypeDefinitionName))
|
|
{
|
|
instance.TypeDefinition = new XmlQualifiedName(
|
|
c.TypeDefinitionName,
|
|
c.TypeDefinitionNamespace ?? string.Empty);
|
|
}
|
|
if (instance is VariableDesign variable)
|
|
{
|
|
if (!string.IsNullOrEmpty(c.DataTypeName))
|
|
{
|
|
variable.DataType = new XmlQualifiedName(
|
|
c.DataTypeName,
|
|
c.DataTypeNamespace ?? string.Empty);
|
|
}
|
|
variable.ValueRank = (ValueRank)c.ValueRank;
|
|
variable.ValueRankSpecified = c.ValueRank != (int)ValueRank.Scalar;
|
|
}
|
|
else if (instance is MethodDesign method)
|
|
{
|
|
// Methods carry their own argument lists; the TypeDefinition
|
|
// would point at a MethodType (a separate MethodDesign in
|
|
// the upstream model) which is NOT carried by the dependency
|
|
// payload, so clear it to avoid a downstream
|
|
// FindNode<MethodDesign> failure during ValidateInstance.
|
|
method.TypeDefinition = null;
|
|
method.InputArguments = MaterialiseMethodArgs(c.InputArguments);
|
|
method.OutputArguments = MaterialiseMethodArgs(c.OutputArguments);
|
|
method.HasArguments = method.InputArguments.Length > 0 ||
|
|
method.OutputArguments.Length > 0;
|
|
}
|
|
// Mark as inherited-declaration so consumer code paths
|
|
// that iterate children for emission can short-circuit.
|
|
instance.IsDeclaration = true;
|
|
return instance;
|
|
}
|
|
|
|
private static Parameter[] MaterialiseMethodArgs(
|
|
IReadOnlyList<DependencyMethodArg> args)
|
|
{
|
|
if (args == null || args.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
var result = new Parameter[args.Count];
|
|
for (int i = 0; i < args.Count; i++)
|
|
{
|
|
DependencyMethodArg a = args[i];
|
|
result[i] = new Parameter
|
|
{
|
|
Name = a.Name ?? string.Empty,
|
|
DataType = new XmlQualifiedName(
|
|
a.DataTypeName ?? string.Empty,
|
|
a.DataTypeNamespace ?? string.Empty),
|
|
ValueRank = (ValueRank)a.ValueRank,
|
|
Description = new LocalizedText()
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve <c>TypeDefinitionNode</c> / <c>DataTypeNode</c> /
|
|
/// <c>BaseTypeNode</c> references on dependency-materialised
|
|
/// types and their children. Run after all dependencies have
|
|
/// been applied so cross-dependency references can resolve.
|
|
/// </summary>
|
|
private void LinkDependencyChildren()
|
|
{
|
|
if (m_pendingDependencies == null || m_pendingDependencies.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
foreach (NodeDesign node in m_nodes.Values)
|
|
{
|
|
if (node is not TypeDesign type)
|
|
{
|
|
continue;
|
|
}
|
|
if (!type.IsDeclaration)
|
|
{
|
|
continue;
|
|
}
|
|
// Resolve BaseTypeNode.
|
|
if (type.BaseType != null &&
|
|
type.BaseTypeNode == null &&
|
|
m_nodes.TryGetValue(type.BaseType, out NodeDesign baseDesign))
|
|
{
|
|
type.BaseTypeNode = baseDesign as TypeDesign;
|
|
}
|
|
// Resolve children's TypeDefinitionNode / DataTypeNode.
|
|
if (!type.HasChildren || type.Children?.Items == null)
|
|
{
|
|
continue;
|
|
}
|
|
foreach (InstanceDesign instance in type.Children.Items)
|
|
{
|
|
if (instance.TypeDefinition != null &&
|
|
instance.TypeDefinitionNode == null &&
|
|
m_nodes.TryGetValue(instance.TypeDefinition, out NodeDesign tdNode))
|
|
{
|
|
instance.TypeDefinitionNode = tdNode as TypeDesign;
|
|
}
|
|
if (instance is VariableDesign variable &&
|
|
variable.DataType != null &&
|
|
variable.DataTypeNode == null &&
|
|
m_nodes.TryGetValue(variable.DataType, out NodeDesign dtNode))
|
|
{
|
|
variable.DataTypeNode = dtNode as DataTypeDesign;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<PendingDependency> m_pendingDependencies;
|
|
|
|
private sealed record PendingDependency(ModelDependencyV1 Dependency, string Prefix, string Name);
|
|
}
|
|
}
|