mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
2de64d7d93
# Description A reference in a NodeSet2 input whose target is a node of another model was dropped without a diagnostic when that model is supplied by a referenced assembly rather than by an AdditionalFiles input. The dependency metadata of a referenced assembly carries types only, so the importer could not resolve an instance such as DI's DeviceSet and skipped every reference to it: a device organized by DeviceSet (inverse Organizes) lost that reference, and so did any non-hierarchical reference to such a node. When the target of a reference is in another, non-zero namespace and no loaded design defines it, the importer now creates a stand-in that carries only the NodeId the generated AddReference call needs. The stand-in is not generated. References whose reference type is unknown are still skipped. Tests generate a DI-style NodeSet2 whose device is organized by DeviceSet against DI supplied as a referenced assembly and as an AdditionalFile, and compile the result against the stack. ## Related Issues None. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
5155 lines
210 KiB
C#
5155 lines
210 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.Reflection;
|
|
using System.Xml;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua.Schema.Model;
|
|
using Opc.Ua.Types;
|
|
|
|
namespace Opc.Ua.SourceGeneration
|
|
{
|
|
/// <summary>
|
|
/// Generates data types and node state classes.
|
|
/// </summary>
|
|
internal sealed class NodeStateGenerator : IGenerator
|
|
{
|
|
/// <summary>
|
|
/// Create node state generator
|
|
/// </summary>
|
|
public NodeStateGenerator(IGeneratorContext context)
|
|
{
|
|
m_context = context ?? throw new ArgumentNullException(nameof(context));
|
|
m_messageContext = ServiceMessageContext.CreateEmpty(context.Telemetry);
|
|
m_logger = context.Telemetry.CreateLogger<NodeStateGenerator>();
|
|
CollectNodesToGenerate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// When <c>true</c>, emits the model's public NodeSet import factory
|
|
/// provider. Only set for models whose consumer references
|
|
/// <c>Opc.Ua.Server</c>.
|
|
/// </summary>
|
|
public bool GenerateNodeSetImportSupport { get; init; }
|
|
|
|
/// <summary>
|
|
/// When <c>true</c>, emits the import factory provider on its own and
|
|
/// leaves the model types to the assembly that owns them. Set when a
|
|
/// node manager binds to a model a referenced assembly supplies: the
|
|
/// provider implements an <c>Opc.Ua.Server</c> contract that a
|
|
/// model-only assembly cannot reference, so it has to be emitted
|
|
/// beside the manager rather than beside the model.
|
|
/// </summary>
|
|
public bool ImportSupportOnly { get; init; }
|
|
|
|
/// <inheritdoc/>
|
|
public IEnumerable<Resource> Emit()
|
|
{
|
|
m_initializers.Clear();
|
|
if (ImportSupportOnly)
|
|
{
|
|
return GenerateNodeSetImportSupport
|
|
? [EmitNodeSetImportSupport()]
|
|
: [];
|
|
}
|
|
if (m_instances.Count + m_nodes.Count == 0)
|
|
{
|
|
return GenerateNodeSetImportSupport
|
|
? [EmitNodeSetImportSupport()]
|
|
: [];
|
|
}
|
|
List<Resource> resources =
|
|
[
|
|
EmitNodeStateClasses(),
|
|
EmitExtensions()
|
|
];
|
|
if (GenerateNodeSetImportSupport)
|
|
{
|
|
resources.Add(EmitNodeSetImportSupport());
|
|
}
|
|
Resource? initializers = EmbedInitializers();
|
|
if (initializers != null)
|
|
{
|
|
resources.Add(initializers);
|
|
}
|
|
return resources;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the nodestates classes
|
|
/// </summary>
|
|
private TextFileResource EmitNodeStateClasses()
|
|
{
|
|
string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix;
|
|
string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format(
|
|
"{0}.NodeStates.g.cs",
|
|
nsPrefix));
|
|
using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName);
|
|
using var templateWriter = new TemplateWriter(writer);
|
|
var template = new Template(templateWriter, NodeStateTemplates.File);
|
|
template.AddReplacement(Tokens.NamespacePrefix, nsPrefix);
|
|
template.AddReplacement(
|
|
Tokens.Namespace,
|
|
nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal));
|
|
template.AddReplacement(
|
|
Tokens.NamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
|
|
m_context.ModelDesign.TargetNamespace.Value));
|
|
|
|
// TODO: Switch to "instance" list and get "type" from entry.IsInstanceOfType
|
|
template.AddReplacement(
|
|
Tokens.ListOfTypes,
|
|
m_nodes.Values,
|
|
LoadTemplate_ListOfNodeStateClasses,
|
|
WriteTemplate_ListOfNodeStateClasses);
|
|
template.AddReplacement(
|
|
Tokens.ListOfTypeActivators,
|
|
NodeStateTemplates.ActivatorClass,
|
|
m_nodes.Values,
|
|
LoadTemplate_ListOfNodeStateActivators,
|
|
WriteTemplate_ListOfNodeStateActivators);
|
|
|
|
template.Render();
|
|
return fileName.AsTextFileResource();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create extensions
|
|
/// </summary>
|
|
/// <summary>
|
|
/// Create the model's NodeSet import factory provider
|
|
/// </summary>
|
|
private TextFileResource EmitNodeSetImportSupport()
|
|
{
|
|
string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix;
|
|
string typeStem = nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal);
|
|
string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format(
|
|
"{0}.NodeSetImportSupport.g.cs",
|
|
nsPrefix));
|
|
using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName);
|
|
using var templateWriter = new TemplateWriter(writer);
|
|
var template = new Template(
|
|
templateWriter,
|
|
NodeStateTemplates.NodeSetImportSupport_File);
|
|
template.AddReplacement(Tokens.NamespacePrefix, nsPrefix);
|
|
template.AddReplacement(
|
|
Tokens.ListOfTypes,
|
|
["body"],
|
|
onLoad: context =>
|
|
{
|
|
EmitNodeSetImportFactoryProvider(
|
|
context.Out,
|
|
typeStem + "NodeSetImportFactoryProvider");
|
|
return null;
|
|
});
|
|
template.Render();
|
|
return fileName.AsTextFileResource();
|
|
}
|
|
|
|
private void EmitNodeSetImportFactoryProvider(
|
|
ITemplateWriter writer,
|
|
string className)
|
|
{
|
|
List<ImportFactoryRegistration> registrations =
|
|
GetNodeSetImportFactoryRegistrations();
|
|
|
|
writer.WriteLine(" /// <summary>");
|
|
writer.WriteLine(
|
|
" /// Public registration provider for typed NodeSet imports.");
|
|
writer.WriteLine(" /// </summary>");
|
|
writer.WriteLine(
|
|
" [global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{0}\", \"{1}\")]",
|
|
typeof(NodeStateGenerator).Assembly.GetName().Name,
|
|
typeof(NodeStateGenerator).Assembly.GetName().Version);
|
|
writer.WriteLine(
|
|
" public sealed class {0} :",
|
|
className);
|
|
writer.WriteLine(
|
|
" global::Opc.Ua.Server.Nodes.INodeSetImportFactoryProvider");
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(" /// <summary>");
|
|
writer.WriteLine(" /// Initializes a provider.");
|
|
writer.WriteLine(" /// </summary>");
|
|
writer.WriteLine(" public {0}()", className);
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(" }");
|
|
writer.WriteLine();
|
|
writer.WriteLine(" /// <summary>");
|
|
writer.WriteLine(" /// Gets the shared provider instance.");
|
|
writer.WriteLine(" /// </summary>");
|
|
writer.WriteLine(
|
|
" public static {0} Instance {{ get; }} = new {0}();",
|
|
className);
|
|
writer.WriteLine();
|
|
writer.WriteLine(" /// <inheritdoc/>");
|
|
writer.WriteLine(
|
|
" public global::Opc.Ua.ArrayOf<global::Opc.Ua.Server.Nodes.INodeSetImportFactory>");
|
|
writer.WriteLine(" GetNodeSetImportFactories()");
|
|
writer.WriteLine(" {");
|
|
if (registrations.Count == 0)
|
|
{
|
|
writer.WriteLine(
|
|
" return new global::Opc.Ua.ArrayOf<" +
|
|
"global::Opc.Ua.Server.Nodes.INodeSetImportFactory>(");
|
|
writer.WriteLine(
|
|
" global::System.Array.Empty<" +
|
|
"global::Opc.Ua.Server.Nodes.INodeSetImportFactory>());");
|
|
}
|
|
else
|
|
{
|
|
writer.WriteLine(
|
|
" return new global::Opc.Ua.ArrayOf<" +
|
|
"global::Opc.Ua.Server.Nodes.INodeSetImportFactory>(");
|
|
writer.WriteLine(
|
|
" new global::Opc.Ua.Server.Nodes.INodeSetImportFactory[]");
|
|
writer.WriteLine(" {");
|
|
for (int i = 0; i < registrations.Count; i++)
|
|
{
|
|
writer.WriteLine(
|
|
" ImportFactory{0:D4}.Instance{1}",
|
|
i,
|
|
i + 1 == registrations.Count ? string.Empty : ",");
|
|
}
|
|
writer.WriteLine(" });");
|
|
}
|
|
writer.WriteLine(" }");
|
|
|
|
for (int i = 0; i < registrations.Count; i++)
|
|
{
|
|
EmitNodeSetImportFactory(writer, registrations[i], i);
|
|
}
|
|
|
|
writer.WriteLine(" }");
|
|
}
|
|
|
|
private static void EmitNodeSetImportFactory(
|
|
ITemplateWriter writer,
|
|
ImportFactoryRegistration registration,
|
|
int index)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
" private sealed class ImportFactory{0:D4} :",
|
|
index);
|
|
writer.WriteLine(
|
|
" global::Opc.Ua.Server.Nodes.INodeSetImportFactory");
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(
|
|
" private ImportFactory{0:D4}()",
|
|
index);
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(" }");
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
" public static ImportFactory{0:D4} Instance {{ get; }} = new ImportFactory{0:D4}();",
|
|
index);
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
" public global::Opc.Ua.NodeClass NodeClass");
|
|
writer.WriteLine(
|
|
" => global::Opc.Ua.NodeClass.{0};",
|
|
registration.NodeClass);
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
" public global::Opc.Ua.Server.Nodes.NodeSetImportDiscriminator Discriminator");
|
|
writer.WriteLine(
|
|
" => global::Opc.Ua.Server.Nodes.NodeSetImportDiscriminator.{0};",
|
|
registration.Discriminator);
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
" public global::Opc.Ua.ExpandedNodeId DiscriminatorId");
|
|
writer.WriteLine(
|
|
" => {0};",
|
|
registration.DiscriminatorId);
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
" public global::Opc.Ua.NodeState CreateEmptyState()");
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(
|
|
" return {0};",
|
|
registration.CreateExpression);
|
|
writer.WriteLine(" }");
|
|
writer.WriteLine(" }");
|
|
}
|
|
|
|
private List<ImportFactoryRegistration>
|
|
GetNodeSetImportFactoryRegistrations()
|
|
{
|
|
var registrations = new Dictionary<string, ImportFactoryRegistration>(
|
|
StringComparer.Ordinal);
|
|
string targetNamespace = m_context.ModelDesign.TargetNamespace.Value;
|
|
|
|
void Add(
|
|
string nodeClass,
|
|
string discriminator,
|
|
NodeDesign discriminatorNode,
|
|
string? createExpression)
|
|
{
|
|
if (discriminatorNode == null ||
|
|
string.IsNullOrEmpty(createExpression) ||
|
|
!HasResolvableNodeId(discriminatorNode))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string discriminatorId = discriminatorNode.GetExpandedNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces);
|
|
string key = nodeClass + "|" + discriminator + "|" + discriminatorId;
|
|
registrations.TryAdd(
|
|
key,
|
|
new ImportFactoryRegistration(
|
|
nodeClass,
|
|
discriminator,
|
|
discriminatorId,
|
|
createExpression));
|
|
}
|
|
|
|
foreach (NodeToGenerate node in m_nodes.Values)
|
|
{
|
|
if (node.Parent != null ||
|
|
!string.Equals(
|
|
node.Design!.SymbolicId.Namespace,
|
|
targetNamespace,
|
|
StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
switch (node.Design)
|
|
{
|
|
case ObjectTypeDesign objectType:
|
|
Add(
|
|
"ObjectType",
|
|
"NodeId",
|
|
objectType,
|
|
"new global::Opc.Ua.BaseObjectTypeState()");
|
|
if (!objectType.IsAbstract && node.Instance != null)
|
|
{
|
|
Add(
|
|
"Object",
|
|
"TypeDefinition",
|
|
objectType,
|
|
GetDirectEmptyStateCreation(
|
|
(InstanceDesign)node.Instance.Design!));
|
|
}
|
|
break;
|
|
case VariableTypeDesign variableType:
|
|
Add(
|
|
"VariableType",
|
|
"NodeId",
|
|
variableType,
|
|
"new global::Opc.Ua.BaseDataVariableTypeState()");
|
|
if (!variableType.IsAbstract && node.Instance != null)
|
|
{
|
|
Add(
|
|
"Variable",
|
|
"TypeDefinition",
|
|
variableType,
|
|
GetDirectEmptyStateCreation(
|
|
(InstanceDesign)node.Instance.Design!));
|
|
}
|
|
break;
|
|
case DataTypeDesign dataType:
|
|
Add(
|
|
"DataType",
|
|
"NodeId",
|
|
dataType,
|
|
"new global::Opc.Ua.DataTypeState()");
|
|
break;
|
|
case ReferenceTypeDesign referenceType:
|
|
Add(
|
|
"ReferenceType",
|
|
"NodeId",
|
|
referenceType,
|
|
"new global::Opc.Ua.ReferenceTypeState()");
|
|
break;
|
|
case ViewDesign view:
|
|
Add(
|
|
"View",
|
|
"NodeId",
|
|
view,
|
|
"new global::Opc.Ua.ViewState()");
|
|
break;
|
|
case MethodDesign method
|
|
when method.IsMethodTypeDesign():
|
|
string? createExpression =
|
|
GetDirectEmptyStateCreation(method);
|
|
Add(
|
|
"Method",
|
|
"NodeId",
|
|
method,
|
|
createExpression);
|
|
Add(
|
|
"Method",
|
|
"MethodDeclaration",
|
|
method,
|
|
createExpression);
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach (NodeToGenerate node in m_nodes.Values)
|
|
{
|
|
if (node.Design is MethodDesign method &&
|
|
node.RootIsTypeDefinition &&
|
|
!method.IsMethodTypeDesign() &&
|
|
string.Equals(
|
|
method.SymbolicId.Namespace,
|
|
targetNamespace,
|
|
StringComparison.Ordinal))
|
|
{
|
|
Add(
|
|
"Method",
|
|
"MethodDeclaration",
|
|
method,
|
|
GetDirectEmptyStateCreation(method));
|
|
}
|
|
}
|
|
|
|
foreach (NodeToGenerate node in m_nodes.Values)
|
|
{
|
|
if (node.Design is not InstanceDesign instance ||
|
|
!string.Equals(
|
|
instance.SymbolicId.Namespace,
|
|
targetNamespace,
|
|
StringComparison.Ordinal) ||
|
|
!IsInAddressSpace(node))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string? nodeClass = GetImportNodeClass(instance);
|
|
if (nodeClass == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only declarations get an exact-NodeId factory. Registering
|
|
// one per address-space instance would emit a factory per node
|
|
// of the model, and an imported node that re-declares an
|
|
// instance still resolves through its TypeDefinition or
|
|
// MethodDeclaration below.
|
|
if (IsDeclarationNode(node))
|
|
{
|
|
Add(
|
|
nodeClass,
|
|
"NodeId",
|
|
instance,
|
|
GetDirectEmptyStateCreation(instance));
|
|
}
|
|
|
|
if (instance is ObjectDesign &&
|
|
instance.TypeDefinitionNode is ObjectTypeDesign objectType &&
|
|
string.Equals(
|
|
objectType.SymbolicId.Namespace,
|
|
targetNamespace,
|
|
StringComparison.Ordinal))
|
|
{
|
|
Add(
|
|
"Object",
|
|
"TypeDefinition",
|
|
objectType,
|
|
GetDirectTypeInstanceCreation(objectType, instance));
|
|
}
|
|
else if (instance is VariableDesign &&
|
|
instance.TypeDefinitionNode is VariableTypeDesign variableType &&
|
|
string.Equals(
|
|
variableType.SymbolicId.Namespace,
|
|
targetNamespace,
|
|
StringComparison.Ordinal))
|
|
{
|
|
Add(
|
|
"Variable",
|
|
"TypeDefinition",
|
|
variableType,
|
|
GetDirectTypeInstanceCreation(variableType, instance));
|
|
}
|
|
|
|
if (instance is MethodDesign method &&
|
|
method.MethodDeclarationNode != null)
|
|
{
|
|
Add(
|
|
"Method",
|
|
"MethodDeclaration",
|
|
method.MethodDeclarationNode,
|
|
GetDirectEmptyStateCreation(method));
|
|
}
|
|
}
|
|
|
|
return
|
|
[
|
|
.. registrations.Values
|
|
.OrderBy(
|
|
registration => registration.Discriminator,
|
|
StringComparer.Ordinal)
|
|
.ThenBy(
|
|
registration => registration.NodeClass,
|
|
StringComparer.Ordinal)
|
|
.ThenBy(
|
|
registration => registration.DiscriminatorId,
|
|
StringComparer.Ordinal)
|
|
];
|
|
}
|
|
|
|
private string? GetDirectTypeInstanceCreation(
|
|
TypeDesign type,
|
|
InstanceDesign fallbackInstance)
|
|
{
|
|
if (m_nodes.TryGetValue(
|
|
type.SymbolicId,
|
|
out NodeToGenerate? typeNode) &&
|
|
typeNode.Instance?.Design is InstanceDesign typeInstance)
|
|
{
|
|
return GetDirectEmptyStateCreation(typeInstance);
|
|
}
|
|
return GetDirectEmptyStateCreation(fallbackInstance);
|
|
}
|
|
|
|
private string? GetDirectEmptyStateCreation(InstanceDesign? instance)
|
|
{
|
|
string factory = instance.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
asFactory: true);
|
|
return ConvertStateFactoryToDirectConstructor(factory);
|
|
}
|
|
|
|
private string? GetDirectEmptyStateCreation(MethodDesign method)
|
|
{
|
|
string factory = method.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
asFactory: true);
|
|
return ConvertStateFactoryToDirectConstructor(factory);
|
|
}
|
|
|
|
private static string? ConvertStateFactoryToDirectConstructor(
|
|
string factory)
|
|
{
|
|
if (string.IsNullOrEmpty(factory))
|
|
{
|
|
return null;
|
|
}
|
|
if (factory.StartsWith("new ", StringComparison.Ordinal))
|
|
{
|
|
return factory + "(null)";
|
|
}
|
|
|
|
const string withMarker = ".With<";
|
|
int withIndex = factory.LastIndexOf(
|
|
withMarker,
|
|
StringComparison.Ordinal);
|
|
if (withIndex >= 0)
|
|
{
|
|
string stateType = factory[..withIndex];
|
|
string builderType = factory[(withIndex + ".With".Length)..];
|
|
return "new " +
|
|
stateType +
|
|
".Implementation" +
|
|
builderType +
|
|
"(null)";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether a node belongs to a declaration - a type definition or
|
|
/// a method type - rather than to the model's address space.
|
|
/// </summary>
|
|
private static bool IsDeclarationNode(NodeToGenerate node)
|
|
{
|
|
NodeToGenerate root = node;
|
|
while (root.Parent != null)
|
|
{
|
|
root = root.Parent;
|
|
}
|
|
return root.RootIsTypeDefinition ||
|
|
(root.Design is MethodDesign method && method.IsMethodTypeDesign());
|
|
}
|
|
|
|
private static string? GetImportNodeClass(InstanceDesign instance)
|
|
{
|
|
return instance switch
|
|
{
|
|
ObjectDesign => "Object",
|
|
VariableDesign => "Variable",
|
|
MethodDesign => "Method",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private TextFileResource EmitExtensions()
|
|
{
|
|
string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix;
|
|
string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format(
|
|
"{0}.NodeStates.ex.g.cs",
|
|
nsPrefix));
|
|
using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName);
|
|
using var templateWriter = new TemplateWriter(writer);
|
|
var template = new Template(
|
|
templateWriter,
|
|
NodeStateTemplates.Extensions_File);
|
|
|
|
template.AddReplacement(Tokens.NamespacePrefix, nsPrefix);
|
|
template.AddReplacement(
|
|
Tokens.Namespace,
|
|
nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal));
|
|
template.AddReplacement(
|
|
Tokens.NamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
|
|
m_context.ModelDesign.TargetNamespace.Value));
|
|
|
|
template.AddReplacement(
|
|
Tokens.ListOfNodeStateInitializers,
|
|
m_nodes.Values,
|
|
LoadTemplate_ListOfNodeStateInitializers,
|
|
WriteTemplate_ListOfNodeStateInitializers);
|
|
template.AddReplacement(
|
|
Tokens.ListOfNodeStateTypeFactories,
|
|
m_nodes.Values,
|
|
LoadTemplate_ListOfNodeStateFactories,
|
|
WriteTemplate_ListOfNodeStateFactories);
|
|
|
|
template.AddReplacement(
|
|
Tokens.ListOfNodeStateInstanceFactories,
|
|
m_instances.Values,
|
|
LoadTemplate_ListOfNodeStateFactories,
|
|
WriteTemplate_ListOfNodeStateFactories);
|
|
|
|
// TODO: Adopt to "instance" list and get "type" from entry.IsInstanceOfType
|
|
template.AddReplacement(
|
|
Tokens.ListOfActivatorRegistrations,
|
|
NodeStateTemplates.ActivatorRegistration,
|
|
m_nodes.Values,
|
|
LoadTemplate_ListOfNodeStateActivators,
|
|
WriteTemplate_ListOfNodeStateActivators);
|
|
|
|
template.Render();
|
|
return fileName.AsTextFileResource();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfNodeStateActivators(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return null;
|
|
}
|
|
if (ExcludeNodeStateClassGeneration(node))
|
|
{
|
|
return null;
|
|
}
|
|
switch (node.Design)
|
|
{
|
|
case ObjectTypeDesign:
|
|
case VariableTypeDesign:
|
|
case MethodDesign method
|
|
when MethodDesignArgumentResolver.HasMethodArguments(method) &&
|
|
method.IsMethodTypeDesign():
|
|
return context.TemplateString;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private bool WriteTemplate_ListOfNodeStateActivators(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return false;
|
|
}
|
|
string nodeClass = node.Design!.GetNodeClassAsString();
|
|
context.Template.AddReplacement(Tokens.NodeClass, nodeClass);
|
|
context.Template.AddBrowseNameReplacement(
|
|
Tokens.BrowseName,
|
|
Tokens.BrowseNameLiteral,
|
|
node.Design!.SymbolicName.Name,
|
|
m_logger);
|
|
context.Template.AddReplacement(Tokens.SymbolicId, node.Design.SymbolicId.Name);
|
|
if (node.Design is TypeDesign type)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassName,
|
|
CoreUtils.Format("{0}State", type.ClassName));
|
|
}
|
|
else if (node.Design is MethodDesign method)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassName,
|
|
method.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value, []));
|
|
}
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfNodeStateClasses(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return null;
|
|
}
|
|
if (ExcludeNodeStateClassGeneration(node))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return node.Design switch
|
|
{
|
|
ObjectTypeDesign
|
|
=> NodeStateTemplates.ObjectType_Class,
|
|
VariableTypeDesign
|
|
=> NodeStateTemplates.VariableType_Class,
|
|
MethodDesign method when
|
|
MethodDesignArgumentResolver.HasMethodArguments(method)
|
|
=> NodeStateTemplates.MethodType_Class,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private bool WriteTemplate_ListOfNodeStateClasses(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return false;
|
|
}
|
|
NodeDesign? root = node.Design;
|
|
context.Template.AddReplacement(Tokens.NodeClass, root!.GetNodeClassAsString());
|
|
context.Template.AddReplacement(Tokens.TypeName, root!.SymbolicName.Name);
|
|
context.Template.AddReplacement(Tokens.SymbolicId, root.SymbolicId.Name);
|
|
context.Template.AddBrowseNameReplacement(
|
|
Tokens.BrowseName,
|
|
Tokens.BrowseNameLiteral,
|
|
root.SymbolicName.Name,
|
|
m_logger);
|
|
context.Template.AddReplacement(
|
|
Tokens.Description,
|
|
root.Description != null ? root.Description.Value : string.Empty);
|
|
context.Template.AddReplacement(
|
|
Tokens.NamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
|
|
root.SymbolicName.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.NamespacePrefix,
|
|
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
|
|
root.SymbolicId.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.XmlNamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetConstantForXmlNamespace(
|
|
root.SymbolicId.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.BrowseNameNamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetNamespaceUriAsCode(
|
|
root.SymbolicName.Namespace));
|
|
|
|
switch (root)
|
|
{
|
|
case MethodDesign method:
|
|
AddNodeStateClassMethodTypeReplacements(context, method);
|
|
break;
|
|
case ObjectTypeDesign objectType:
|
|
AddNodeStateClassObjectTypeReplacements(context, objectType);
|
|
break;
|
|
case VariableTypeDesign variableType:
|
|
AddNodeStateClassVariableTypeReplacements(context, variableType);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.InitializeOptionalChildren,
|
|
NodeStateTemplates.InitializeOptionalChild,
|
|
node.Children.Values,
|
|
LoadTemplate_InitializeOptionalChildren,
|
|
WriteTemplate_InitializeOptionalChildren);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfNonMandatoryChildren,
|
|
node.Children.Values,
|
|
LoadTemplate_ListOfNonMandatoryChildren,
|
|
WriteTemplate_ListOfNonMandatoryChildren);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfFields,
|
|
node.Children.Values,
|
|
LoadTemplate_ListOfFields);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfProperties,
|
|
node.Children.Values,
|
|
LoadTemplate_ListOfProperties,
|
|
WriteTemplate_ListOfProperties);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfChildOperations,
|
|
NodeStateTemplates.ChildOperations,
|
|
[node],
|
|
LoadTemplate_FindChildMethods,
|
|
WriteTemplate_FindChildMethods);
|
|
|
|
List<InstanceDesign> properties =
|
|
GetChildrenWithProperties(node, true);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfChildCopies,
|
|
NodeStateTemplates.CloneChild,
|
|
properties,
|
|
WriteTemplate_ListOfChildren);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfChildHashes,
|
|
NodeStateTemplates.HashChild,
|
|
properties,
|
|
WriteTemplate_ListOfChildren);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfEqualityComparers,
|
|
NodeStateTemplates.CompareChild,
|
|
properties,
|
|
WriteTemplate_ListOfChildren);
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfNonMandatoryChildren(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (node.IsNotExplicitlyDefined)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
if (instance.IsOverriddenWithSameClass(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
switch (GetEffectiveModellingRule(node, instance))
|
|
{
|
|
case ModellingRule.Optional:
|
|
return NodeStateTemplates.OptionalMethod;
|
|
case ModellingRule.OptionalPlaceholder:
|
|
case ModellingRule.MandatoryPlaceholder:
|
|
return NodeStateTemplates.PlaceHolderMethod;
|
|
case ModellingRule.ExposesItsArray:
|
|
case ModellingRule.CardinalityRestriction:
|
|
case ModellingRule.MandatoryShared:
|
|
// TODO?
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool WriteTemplate_ListOfNonMandatoryChildren(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
context.Template.AddReplacement(Tokens.AccessorSymbol, "public new");
|
|
if (!instance.IsOverridden())
|
|
{
|
|
if (!s_builtInMethodNames.Contains(instance.SymbolicName.Name) &&
|
|
!HidesInheritedPlaceholderHelper(instance))
|
|
{
|
|
context.Template.AddReplacement(Tokens.AccessorSymbol, "public");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
instance = instance.GetMergedInstance();
|
|
}
|
|
|
|
context.Template.AddReplacement(Tokens.Description,
|
|
instance.Description != null ? instance.Description.Value : string.Empty);
|
|
context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
|
|
context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name);
|
|
context.Template.AddReplacement(
|
|
Tokens.NodeIdConstant,
|
|
instance.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the CLR helper signature rather than the qualified browse name: a placeholder
|
|
/// in a different wire namespace can still hide the same Add helper on a generated base class.
|
|
/// </summary>
|
|
private static bool HidesInheritedPlaceholderHelper(InstanceDesign instance)
|
|
{
|
|
if (instance.ModellingRule is not ModellingRule.MandatoryPlaceholder and
|
|
not ModellingRule.OptionalPlaceholder ||
|
|
instance.Parent is not TypeDesign declaringType)
|
|
{
|
|
return false;
|
|
}
|
|
for (TypeDesign? baseType = declaringType.BaseTypeNode; baseType != null; baseType = baseType.BaseTypeNode)
|
|
{
|
|
if (baseType.Children?.Items == null)
|
|
{
|
|
continue;
|
|
}
|
|
foreach (InstanceDesign child in baseType.Children.Items)
|
|
{
|
|
if (child.ModellingRule is
|
|
ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder &&
|
|
child.SymbolicName.Name == instance.SymbolicName.Name)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_TypedVariableType(ILoadContext context)
|
|
{
|
|
if (context.Target is not VariableTypeDesign variableType)
|
|
{
|
|
return null;
|
|
}
|
|
if (variableType.DataTypeNode!.IsTemplateParameterRequired(variableType.ValueRank))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return context.TemplateString;
|
|
}
|
|
|
|
private bool WriteTemplate_TypedVariableType(IWriteContext context)
|
|
{
|
|
if (context.Target is not VariableTypeDesign type)
|
|
{
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(Tokens.NodeClass, type.GetNodeClassAsString());
|
|
context.Template.AddReplacement(Tokens.ClassName, type.ClassName);
|
|
context.Template.AddReplacement(Tokens.TypeName, type.SymbolicName.Name);
|
|
context.Template.AddBrowseNameReplacement(
|
|
Tokens.BrowseName,
|
|
Tokens.BrowseNameLiteral,
|
|
type.SymbolicName.Name,
|
|
m_logger);
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_VariableTypeValue(ILoadContext context)
|
|
{
|
|
if (context.Target is not VariableTypeDesign variableType)
|
|
{
|
|
return null;
|
|
}
|
|
Dictionary<string, Parameter> fields = [];
|
|
CollectMatchingFields(variableType, fields);
|
|
|
|
if (fields.Count == 0)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
return context.TemplateString;
|
|
}
|
|
|
|
private bool WriteTemplate_VariableTypeValue(IWriteContext context)
|
|
{
|
|
if (context.Target is not VariableTypeDesign type)
|
|
{
|
|
return false;
|
|
}
|
|
Dictionary<string, Parameter> fields = [];
|
|
CollectMatchingFields(type, fields);
|
|
|
|
if (fields.Count == 0)
|
|
|
|
{
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(Tokens.ClassName, type.ClassName);
|
|
context.Template.AddReplacement(Tokens.DataType, type.DataTypeNode!.GetDotNetTypeName(
|
|
ValueRank.Scalar,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
nullable: NullableAnnotation.NonNullable));
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfChildInitializers,
|
|
fields,
|
|
LoadTemplate_VariableTypeValueInitializers);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfUpdateChildrenChangeMasks,
|
|
fields,
|
|
LoadTemplate_VariableTypeValueChangeMasks);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfChildMethods,
|
|
NodeStateTemplates.VariableType_ValueMethods,
|
|
fields,
|
|
WriteTemplate_VariableTypeValueField);
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_VariableTypeValueInitializers(ILoadContext context)
|
|
{
|
|
if (context.Target is not KeyValuePair<string, Parameter> field || field.Value == null)
|
|
{
|
|
return null;
|
|
}
|
|
string name = field.Key;
|
|
string path = field.Key;
|
|
|
|
context.Out.WriteLine("instance = m_variable.{0};", path);
|
|
context.Out.WriteLine("if (instance != null)");
|
|
context.Out.WriteLine("{");
|
|
context.Out.WriteLine(" instance.OnReadValue = OnRead_{0};", name);
|
|
context.Out.WriteLine(" instance.OnWriteValue = OnWrite_{0};", name);
|
|
context.Out.WriteLine(" updateList.Add(instance);");
|
|
context.Out.WriteLine("}");
|
|
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_VariableTypeValueChangeMasks(ILoadContext context)
|
|
{
|
|
if (context.Target is not KeyValuePair<string, Parameter> field ||
|
|
field.Value == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var dataType = field.Value.Parent as DataTypeDesign;
|
|
string path = field.Key;
|
|
// The value is addressed through the generated structure property,
|
|
// which can differ from the authored field name (e.g. a field named
|
|
// like its enclosing type or a reserved member gets a suffix).
|
|
string valuePath = field.Value.GetPropertyName();
|
|
|
|
if (dataType!.IsDotNetEqualityComparable(field.Value.ValueRank))
|
|
{
|
|
context.Out.WriteLine("if (m_value.{0} != newValue.{0})", valuePath);
|
|
}
|
|
else
|
|
{
|
|
context.Out.WriteLine(
|
|
"if (!global::Opc.Ua.CoreUtils.IsEqual(m_value.{0}, newValue.{0}))",
|
|
valuePath);
|
|
}
|
|
context.Out.WriteLine("{");
|
|
context.Out.WriteLine(
|
|
" UpdateChildVariableStatus(m_variable.{0}, ref statusCode, ref timestamp);",
|
|
path);
|
|
context.Out.WriteLine("}");
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool WriteTemplate_VariableTypeValueField(IWriteContext context)
|
|
{
|
|
if (context.Target is not KeyValuePair<string, Parameter> field ||
|
|
field.Value == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
context.Template.AddReplacement(Tokens.ChildName, field.Key);
|
|
context.Template.AddReplacement(Tokens.ChildPath, field.Key);
|
|
context.Template.AddReplacement(Tokens.PropertyName, field.Value.GetPropertyName());
|
|
|
|
string childDataType = field.Value.DataTypeNode!.GetDotNetTypeName(
|
|
field.Value.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
nullable: NullableAnnotation.NonNullable);
|
|
|
|
context.Template.AddReplacement(Tokens.ChildDataType, childDataType);
|
|
|
|
if (field.Value.DataTypeNode!.NeedsCloning())
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueWrite,
|
|
CoreUtils.Format(
|
|
"CopyOnWrite ? ({0})global::Opc.Ua.CoreUtils.Clone(newValue) : newValue",
|
|
childDataType));
|
|
}
|
|
else
|
|
{
|
|
context.Template.AddReplacement(Tokens.ValueWrite, "newValue");
|
|
}
|
|
|
|
AddVariantAccessor(context, field.Value.DataTypeNode!);
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfFields(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
bool isMethodOverrideWithDifferentClass =
|
|
IsMethodOverrideWithDifferentClass(instance);
|
|
if (node.IsNotExplicitlyDefined ||
|
|
(instance.IsOverridden() && !isMethodOverrideWithDifferentClass))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
ModellingRule effectiveRule = GetEffectiveModellingRule(node, instance);
|
|
|
|
if (effectiveRule
|
|
is ModellingRule.ExposesItsArray
|
|
or ModellingRule.MandatoryPlaceholder
|
|
or ModellingRule.OptionalPlaceholder)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (effectiveRule == ModellingRule.None)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
if (instance is MethodDesign method &&
|
|
GetEffectiveModellingRule(node, method) != ModellingRule.Mandatory &&
|
|
GetEffectiveModellingRule(node, method) != ModellingRule.Optional)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (IsBuiltInProperty(node))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (isMethodOverrideWithDifferentClass)
|
|
{
|
|
instance = instance.GetMergedInstance();
|
|
}
|
|
context.Out.WriteLine(
|
|
"private {0}? {1};",
|
|
instance.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces),
|
|
instance.GetChildFieldName());
|
|
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfInputArguments(ILoadContext context)
|
|
{
|
|
if (context.Target is not Parameter field)
|
|
{
|
|
return null;
|
|
}
|
|
string fieldName = GetMethodArgumentIdentifier(field);
|
|
string typeName = field.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
field.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
false);
|
|
|
|
switch (field.DataTypeNode!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined when HasTypedArgumentValueRank(field):
|
|
context.Out.WriteLine(
|
|
"_inputArguments[{2}].TryGetValue(" +
|
|
"out {1} {0}, " +
|
|
"global::Opc.Ua.SystemContextExtensions.AsMessageContext(_context)" +
|
|
");",
|
|
fieldName,
|
|
typeName,
|
|
context.Index);
|
|
break;
|
|
case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar:
|
|
context.Out.WriteLine(
|
|
"{1} {0} = _inputArguments[{2}];",
|
|
fieldName,
|
|
typeName,
|
|
context.Index);
|
|
break;
|
|
default:
|
|
context.Out.WriteLine(
|
|
"_inputArguments[{2}].TryGetValue(out {1} {0});",
|
|
fieldName,
|
|
typeName,
|
|
context.Index);
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfOutputDeclarations(ILoadContext context)
|
|
{
|
|
if (context.Target is not Parameter field)
|
|
{
|
|
return null;
|
|
}
|
|
string fieldName = GetMethodArgumentIdentifier(field);
|
|
string typeName = field.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
field.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
field.IsOptional);
|
|
|
|
switch (field.DataTypeNode!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined when HasTypedArgumentValueRank(field):
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{2}].TryGetValue(" +
|
|
"out {1} {0}, " +
|
|
"global::Opc.Ua.SystemContextExtensions.AsMessageContext(_context)" +
|
|
");",
|
|
fieldName,
|
|
typeName,
|
|
context.Index);
|
|
break;
|
|
case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar:
|
|
context.Out.WriteLine(
|
|
"{1} {0} = _outputArguments[{2}];",
|
|
fieldName,
|
|
typeName,
|
|
context.Index);
|
|
break;
|
|
default:
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{2}].TryGetValue(out {1} {0});",
|
|
fieldName,
|
|
typeName,
|
|
context.Index);
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfOutputArguments(ILoadContext context)
|
|
{
|
|
if (context.Target is not Parameter field)
|
|
{
|
|
return null;
|
|
}
|
|
string fieldName = GetMethodArgumentIdentifier(field);
|
|
switch (field.DataTypeNode!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined when HasTypedArgumentValueRank(field):
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{1}] = global::Opc.Ua.Variant.FromStructure({0});",
|
|
fieldName,
|
|
context.Index);
|
|
break;
|
|
case BasicDataType.BaseDataType:
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{1}] = {0};",
|
|
fieldName,
|
|
context.Index);
|
|
break;
|
|
default:
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{1}] = global::Opc.Ua.Variant.From({0});",
|
|
fieldName,
|
|
context.Index);
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_OnCallDeclaration(ILoadContext context)
|
|
{
|
|
if (context.Target is not MethodDesign method)
|
|
{
|
|
return null;
|
|
}
|
|
(Parameter[] inputArguments, Parameter[] outputArguments) =
|
|
MethodDesignArgumentResolver.ResolveMethodArguments(method);
|
|
AssignMethodArgumentCodeNames(method, inputArguments, outputArguments);
|
|
context.Out.WriteLine("global::Opc.Ua.ISystemContext _context,");
|
|
context.Out.WriteLine("global::Opc.Ua.MethodState _method,");
|
|
context.Out.Write("global::Opc.Ua.NodeId _objectId");
|
|
|
|
for (int ii = 0; ii < inputArguments.Length; ii++)
|
|
{
|
|
Parameter argument = inputArguments[ii];
|
|
|
|
context.Out.WriteLine(",");
|
|
context.Out.Write("{1} {0}", GetMethodArgumentIdentifier(argument),
|
|
argument.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
argument.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
argument.IsOptional));
|
|
}
|
|
|
|
for (int ii = 0; ii < outputArguments.Length; ii++)
|
|
{
|
|
Parameter argument = outputArguments[ii];
|
|
|
|
context.Out.WriteLine(",");
|
|
context.Out.Write("ref {1} {0}", GetMethodArgumentIdentifier(argument),
|
|
argument.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
argument.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
argument.IsOptional));
|
|
}
|
|
|
|
context.Out.WriteLine(");");
|
|
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_OnCallAsyncDeclaration(ILoadContext context)
|
|
{
|
|
if (context.Target is not MethodDesign method)
|
|
{
|
|
return null;
|
|
}
|
|
(Parameter[] inputArguments, Parameter[] outputArguments) =
|
|
MethodDesignArgumentResolver.ResolveMethodArguments(method);
|
|
AssignMethodArgumentCodeNames(method, inputArguments, outputArguments);
|
|
context.Out.WriteLine("global::Opc.Ua.ISystemContext _context,");
|
|
context.Out.WriteLine("global::Opc.Ua.MethodState _method,");
|
|
context.Out.Write("global::Opc.Ua.NodeId _objectId");
|
|
|
|
for (int ii = 0; ii < inputArguments.Length; ii++)
|
|
{
|
|
Parameter argument = inputArguments[ii];
|
|
|
|
context.Out.WriteLine(",");
|
|
context.Out.Write("{1} {0}", GetMethodArgumentIdentifier(argument),
|
|
argument.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
argument.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
argument.IsOptional));
|
|
}
|
|
|
|
context.Out.WriteLine(",");
|
|
context.Out.Write(
|
|
"global::System.Threading.CancellationToken cancellationToken");
|
|
context.Out.WriteLine(");");
|
|
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfOutputArgumentsFromResult(ILoadContext context)
|
|
{
|
|
if (context.Target is not Parameter field)
|
|
{
|
|
return null;
|
|
}
|
|
string fieldName = GetMethodArgumentIdentifier(field, upperCamelCase: true);
|
|
switch (field.DataTypeNode!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined when HasTypedArgumentValueRank(field):
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{1}] = global::Opc.Ua.Variant.FromStructure(_result.{0});",
|
|
fieldName,
|
|
context.Index);
|
|
break;
|
|
case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar:
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{1}] = _result.{0};",
|
|
fieldName,
|
|
context.Index);
|
|
break;
|
|
default:
|
|
context.Out.WriteLine(
|
|
"_outputArguments[{1}] = global::Opc.Ua.Variant.From(_result.{0});",
|
|
fieldName,
|
|
context.Index);
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfResultProperties(ILoadContext context)
|
|
{
|
|
if (context.Target is not Parameter field)
|
|
{
|
|
return null;
|
|
}
|
|
string fieldName = GetMethodArgumentIdentifier(field, upperCamelCase: true);
|
|
context.Out.WriteLine(
|
|
"public {1} {0} {{ get; set; }}",
|
|
fieldName,
|
|
field.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
field.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
field.IsOptional));
|
|
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_OnCallImplementation(ILoadContext context)
|
|
{
|
|
if (context.Target is not MethodDesign method)
|
|
{
|
|
return null;
|
|
}
|
|
(Parameter[] inputArguments, Parameter[] outputArguments) =
|
|
MethodDesignArgumentResolver.ResolveMethodArguments(method);
|
|
AssignMethodArgumentCodeNames(method, inputArguments, outputArguments);
|
|
context.Out.WriteLine("_result = OnCall(");
|
|
context.Out.WriteLine(" _context,");
|
|
context.Out.WriteLine(" this,");
|
|
context.Out.Write(" _objectId");
|
|
|
|
for (int ii = 0; ii < inputArguments.Length; ii++)
|
|
{
|
|
context.Out.WriteLine(",");
|
|
context.Out.Write(" {0}", GetMethodArgumentIdentifier(inputArguments[ii]));
|
|
}
|
|
|
|
for (int ii = 0; ii < outputArguments.Length; ii++)
|
|
{
|
|
context.Out.WriteLine(",");
|
|
context.Out.Write(" ref {0}", GetMethodArgumentIdentifier(outputArguments[ii]));
|
|
}
|
|
|
|
context.Out.WriteLine(");");
|
|
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_OnCallAsyncImplementation(ILoadContext context)
|
|
{
|
|
if (context.Target is not MethodDesign method)
|
|
{
|
|
return null;
|
|
}
|
|
(Parameter[] inputArguments, Parameter[] outputArguments) =
|
|
MethodDesignArgumentResolver.ResolveMethodArguments(method);
|
|
AssignMethodArgumentCodeNames(method, inputArguments, outputArguments);
|
|
context.Out.WriteLine("_result = await OnCallAsync(");
|
|
context.Out.WriteLine(" _context,");
|
|
context.Out.WriteLine(" this,");
|
|
context.Out.Write(" _objectId");
|
|
|
|
for (int ii = 0; ii < inputArguments.Length; ii++)
|
|
{
|
|
context.Out.WriteLine(",");
|
|
context.Out.Write(" {0}", GetMethodArgumentIdentifier(inputArguments[ii]));
|
|
}
|
|
|
|
context.Out.WriteLine(",");
|
|
context.Out.WriteLine(" cancellationToken).ConfigureAwait(false);");
|
|
return null;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_InitializeOptionalChildren(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (node.IsNotExplicitlyDefined)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
if (GetEffectiveModellingRule(node, instance) != ModellingRule.Optional)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (IsBuiltInProperty(node))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return context.TemplateString;
|
|
}
|
|
|
|
private bool WriteTemplate_InitializeOptionalChildren(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance)
|
|
{
|
|
return context.Template.Render();
|
|
}
|
|
|
|
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
|
|
context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name);
|
|
context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName());
|
|
if (instance.Parent is MethodDesign method)
|
|
{
|
|
context.Template.AddReplacement(Tokens.ClassName, method.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
[]));
|
|
}
|
|
else if (instance.Parent is TypeDesign type)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.ClassName,
|
|
type.ClassName);
|
|
}
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfProperties(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (node.IsNotExplicitlyDefined)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
ModellingRule effectiveRule = GetEffectiveModellingRule(node, instance);
|
|
|
|
if (effectiveRule
|
|
is ModellingRule.ExposesItsArray
|
|
or ModellingRule.MandatoryPlaceholder
|
|
or ModellingRule.OptionalPlaceholder)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (effectiveRule == ModellingRule.None)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
if (instance is MethodDesign method &&
|
|
GetEffectiveModellingRule(node, method) != ModellingRule.Mandatory &&
|
|
GetEffectiveModellingRule(node, method) != ModellingRule.Optional)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (instance.IsOverridden())
|
|
{
|
|
if (instance.IsOverriddenWithSameClass(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces))
|
|
{
|
|
return null;
|
|
}
|
|
if (instance is MethodDesign)
|
|
{
|
|
return NodeStateTemplates.Property;
|
|
}
|
|
return NodeStateTemplates.PropertyOverride;
|
|
}
|
|
|
|
if (IsBuiltInProperty(node))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return NodeStateTemplates.Property;
|
|
}
|
|
|
|
private bool WriteTemplate_ListOfProperties(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
context.Template.AddReplacement(Tokens.AccessorSymbol, "public new");
|
|
if (!instance.IsOverridden())
|
|
{
|
|
if (!RequiresNewModifier((node.Parent?.Design)!, instance.SymbolicName.Name) ||
|
|
(instance is VariableDesign && instance.SymbolicName.Name == "Value"))
|
|
{
|
|
context.Template.AddReplacement(Tokens.AccessorSymbol, "public");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
instance = instance.GetMergedInstance();
|
|
}
|
|
|
|
context.Template.AddReplacement(Tokens.Description,
|
|
instance.Description != null ? instance.Description.Value : string.Empty);
|
|
context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
|
|
context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName());
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_FindChildMethods(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return null;
|
|
}
|
|
int count = 0;
|
|
foreach (NodeToGenerate child in node.Children.Values)
|
|
{
|
|
if (child.Design is not InstanceDesign instance)
|
|
{
|
|
continue;
|
|
}
|
|
if (child.IsNotExplicitlyDefined)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
ModellingRule childEffectiveRule =
|
|
GetEffectiveModellingRule(child, instance);
|
|
|
|
if (childEffectiveRule is
|
|
ModellingRule.ExposesItsArray or
|
|
ModellingRule.MandatoryPlaceholder or
|
|
ModellingRule.OptionalPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (childEffectiveRule is ModellingRule.None)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
if (childEffectiveRule is
|
|
ModellingRule.OptionalPlaceholder or
|
|
ModellingRule.MandatoryPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (instance.IsOverriddenWithSameClass(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
if (count == 0)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
return context.TemplateString;
|
|
}
|
|
|
|
private bool WriteTemplate_FindChildMethods(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not TypeDesign type)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
List<InstanceDesign> childrenWithProperties = GetChildrenWithProperties(node);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfFindChildCase,
|
|
NodeStateTemplates.FindChildCase,
|
|
childrenWithProperties,
|
|
WriteTemplate_ListOfChildren);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfCreateOrReplaceChild,
|
|
NodeStateTemplates.CreateOrReplaceChild,
|
|
childrenWithProperties,
|
|
WriteTemplate_ListOfChildren);
|
|
|
|
List<InstanceDesign> additionalChildren = GetAdditionalChildren(node);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfFindChildren,
|
|
NodeStateTemplates.FindChildren,
|
|
additionalChildren,
|
|
WriteTemplate_ListOfChildren);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfRemoveChild,
|
|
NodeStateTemplates.RemoveChild,
|
|
additionalChildren,
|
|
WriteTemplate_ListOfChildren);
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private bool WriteTemplate_ListOfChildren(IWriteContext context)
|
|
{
|
|
if (context.Target is not InstanceDesign instance)
|
|
{
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(Tokens.AccessorSymbol, "public new");
|
|
if (!instance.IsOverridden())
|
|
{
|
|
context.Template.AddReplacement(Tokens.AccessorSymbol, "public");
|
|
}
|
|
|
|
context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(Tokens.ClassFactory, instance.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
asFactory: true));
|
|
context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name);
|
|
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
|
|
// The child's declaration NodeId. CreateOrReplace{Child} uses it to
|
|
// detect a child that still carries the type-level id (and may
|
|
// therefore be rebased onto a per-instance NodeId) versus one whose
|
|
// NodeId the caller already owns.
|
|
context.Template.AddReplacement(
|
|
Tokens.NodeIdConstant,
|
|
instance.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
// The runtime BrowseName.Name may differ from the symbolic name
|
|
// (the design's <opc:BrowseName> attribute can override it, e.g.
|
|
// Boiler's "InputPipe" InstanceDesign with BrowseName "PipeX001").
|
|
// FindChild switches on the runtime browse name, so the case
|
|
// label must use that string verbatim.
|
|
context.Template.AddBrowseNameReplacement(
|
|
Tokens.ChildBrowseName,
|
|
Tokens.ChildBrowseNameLiteral,
|
|
!string.IsNullOrEmpty(instance.BrowseName)
|
|
? instance.BrowseName
|
|
: instance.SymbolicName.Name,
|
|
m_logger);
|
|
context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName());
|
|
context.Template.AddReplacement(Tokens.NodeClass, instance.GetNodeClassAsString());
|
|
context.Template.AddBrowseNameReplacement(
|
|
Tokens.BrowseName,
|
|
Tokens.BrowseNameLiteral,
|
|
instance.SymbolicName.Name,
|
|
m_logger);
|
|
context.Template.AddReplacement(
|
|
Tokens.BrowseNameNamespacePrefix,
|
|
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
|
|
instance.SymbolicName.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.BrowseNameNamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetNamespaceUriAsCode(
|
|
instance.SymbolicName.Namespace));
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfNodeStateInitializers(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return null;
|
|
}
|
|
if (node.Design.IsMethodTypeDesign() ||
|
|
node.InstanceOf != null ||
|
|
!IsInAddressSpace(node) ||
|
|
node.Design!.Purpose == DataTypePurpose.Testing)
|
|
{
|
|
return null;
|
|
}
|
|
if (node.Parent != null)
|
|
{
|
|
// No registering of children
|
|
return null;
|
|
}
|
|
if (node.Design is MethodDesign method)
|
|
{
|
|
return MethodDesignArgumentResolver.HasMethodArguments(method)
|
|
? NodeStateTemplates.AddMethod
|
|
: null;
|
|
}
|
|
return NodeStateTemplates.Add;
|
|
}
|
|
|
|
private bool WriteTemplate_ListOfNodeStateInitializers(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(Tokens.SymbolicId, node.Design!.SymbolicId.Name);
|
|
context.Template.AddReplacement(Tokens.SymbolicName, node.Design.SymbolicName.Name);
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ListOfNodeStateFactories(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return null;
|
|
}
|
|
if (context.Token == Tokens.ListOfNodeStateInstanceFactories)
|
|
{
|
|
if (node.Parent != null)
|
|
{
|
|
return null;
|
|
}
|
|
return node.Design switch
|
|
{
|
|
ObjectDesign => NodeStateTemplates.Create_InstanceOfObjectType,
|
|
VariableDesign => NodeStateTemplates.Create_InstanceOfVariableType,
|
|
MethodDesign => NodeStateTemplates.Create_InstanceOfMethodType,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
if (node.Parent != null)
|
|
{
|
|
if (node.Design is ViewDesign)
|
|
{
|
|
// Views are never emitted as AddChild components (ViewState is
|
|
// not a BaseInstanceState). They are collected as standalone
|
|
// root nodes and linked purely via Organizes references, the
|
|
// same way DataType/ReferenceType type designs are handled.
|
|
return null;
|
|
}
|
|
if (node.Design is not InstanceDesign instance)
|
|
{
|
|
return null;
|
|
}
|
|
if (GetEffectiveModellingRule(node, instance) is
|
|
not ModellingRule.MandatoryPlaceholder and
|
|
not ModellingRule.OptionalPlaceholder)
|
|
{
|
|
return node.Design switch
|
|
{
|
|
ObjectDesign => NodeStateTemplates.Create_ChildObject,
|
|
VariableDesign => NodeStateTemplates.Create_ChildVariable,
|
|
MethodDesign => NodeStateTemplates.Create_ChildMethod,
|
|
_ => null
|
|
};
|
|
}
|
|
return node.Design switch
|
|
{
|
|
ObjectDesign => NodeStateTemplates.Create_ChildObject_Placeholder,
|
|
VariableDesign => NodeStateTemplates.Create_ChildVariable_Placeholder,
|
|
MethodDesign => NodeStateTemplates.Create_ChildMethod_Placeholder,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
return node.Design switch
|
|
{
|
|
ObjectTypeDesign => NodeStateTemplates.Create_ObjectType,
|
|
VariableTypeDesign => NodeStateTemplates.Create_VariableType,
|
|
ReferenceTypeDesign => NodeStateTemplates.Create_ReferenceType,
|
|
DataTypeDesign => NodeStateTemplates.Create_DataType,
|
|
ObjectDesign => NodeStateTemplates.Create_Object,
|
|
VariableDesign => NodeStateTemplates.Create_Variable,
|
|
ViewDesign => NodeStateTemplates.Create_View,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private bool WriteTemplate_ListOfNodeStateFactories(IWriteContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node)
|
|
{
|
|
return false;
|
|
}
|
|
NodeDesign? root = node.Design;
|
|
|
|
// Common replacements for all node types
|
|
context.Template.AddReplacement(
|
|
Tokens.SymbolicId,
|
|
root!.SymbolicId.Name);
|
|
context.Template.AddReplacement(
|
|
Tokens.SymbolicName,
|
|
root.SymbolicName.Name);
|
|
context.Template.AddBrowseNameReplacement(
|
|
Tokens.BrowseName,
|
|
Tokens.BrowseNameLiteral,
|
|
root.SymbolicName.Name,
|
|
m_logger);
|
|
context.Template.AddReplacement(Tokens.NumericIdValue, root.FindNumericIdentifier() ?? 0);
|
|
context.Template.AddReplacement(
|
|
Tokens.TypeName,
|
|
node.InstanceOf?.Design!.SymbolicName.Name);
|
|
context.Template.AddReplacement(
|
|
Tokens.NodeIdConstant,
|
|
root.GetNodeIdAsCode(m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable));
|
|
AddInstanceNodeIdOverrideReplacement(context, node);
|
|
|
|
// .net efficiently interns constant string, so usage of the BrowseName constants is not needed.
|
|
string symbolicNameSymbol = root.SymbolicName.Name.AsStringLiteral();
|
|
context.Template.AddReplacement(Tokens.BrowseNameSymbol,
|
|
string.IsNullOrEmpty(root.BrowseName) ? symbolicNameSymbol : root.BrowseName.AsStringLiteral());
|
|
context.Template.AddReplacement(Tokens.SymbolicNameSymbol, symbolicNameSymbol);
|
|
context.Template.AddReplacement(Tokens.DisplayName, GetDisplayNameValue(root) ?? symbolicNameSymbol);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.BrowseNameNamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetNamespaceUriAsCode(
|
|
root.SymbolicName.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.DescriptionValue,
|
|
GetDescriptionValue(root));
|
|
context.Template.AddReplacement(
|
|
Tokens.WriteMaskValue,
|
|
root.GetWriteMaskAsCode());
|
|
context.Template.AddReplacement(
|
|
Tokens.UserWriteMaskValue,
|
|
root.GetWriteMaskAsCode());
|
|
|
|
// Add Children
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfChildNodeStates,
|
|
node.AllChildren,
|
|
LoadTemplate_ReplaceChild);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfOptionalChildNodeStates,
|
|
node.AllChildren,
|
|
LoadTemplate_ReplaceChild);
|
|
|
|
// Add References
|
|
HashSet<ReferenceToGenerate> references = GetReferences(node);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfReferences,
|
|
references,
|
|
LoadTemplate_AddReference);
|
|
|
|
// Node type-specific replacements
|
|
switch (root)
|
|
{
|
|
case ObjectTypeDesign objectType:
|
|
AddObjectTypeStateFactoryReplacements(context, objectType);
|
|
break;
|
|
case VariableTypeDesign variableType:
|
|
AddVariableTypeStateFactoryReplacements(context, variableType);
|
|
break;
|
|
case ReferenceTypeDesign referenceType:
|
|
AddReferenceTypeStateFactoryReplacements(context, referenceType);
|
|
break;
|
|
case DataTypeDesign dataType:
|
|
AddDataTypeStateFactoryReplacements(context, dataType);
|
|
break;
|
|
case ObjectDesign objectDesign:
|
|
AddObjectReplacements(context, node, objectDesign, references);
|
|
break;
|
|
case VariableDesign variableDesign:
|
|
AddVariableStateFactoryReplacements(context, node, variableDesign, references);
|
|
break;
|
|
case MethodDesign methodDesign:
|
|
AddMethodStateFactoryReplacements(context, node, methodDesign);
|
|
break;
|
|
case ViewDesign viewDesign:
|
|
AddViewStateFactoryReplacements(context, viewDesign);
|
|
break;
|
|
}
|
|
|
|
// Release status
|
|
Export.ReleaseStatus releaseStatus =
|
|
root.ReleaseStatus.ToNodeSetReleaseStatus();
|
|
context.Template.AddReplacement(
|
|
Tokens.ReleaseStatusValue,
|
|
releaseStatus != Export.ReleaseStatus.Released
|
|
? CoreUtils.Format(
|
|
"nodeState.ReleaseStatus = global::Opc.Ua.Export.ReleaseStatus.{0};",
|
|
releaseStatus)
|
|
: null);
|
|
// Categories
|
|
context.Template.AddReplacement(
|
|
Tokens.CategoriesValue,
|
|
!string.IsNullOrEmpty(root.Category)
|
|
? CoreUtils.Format(
|
|
"nodeState.Categories = new string[] {{ {0} }};",
|
|
string.Join(
|
|
", ",
|
|
root.Category
|
|
.Split([','])
|
|
.Select(c => c.Trim().AsStringLiteral())))
|
|
: null);
|
|
// Specification
|
|
context.Template.AddReplacement(
|
|
Tokens.SpecificationValue,
|
|
root.PartNo != 0
|
|
? CoreUtils.Format("nodeState.Specification = \"Part{0}\";", root.PartNo)
|
|
: null);
|
|
|
|
// Access restrictions — emit on all nodes (type and instance).
|
|
// Type hierarchy nodes carry restrictions as metadata but the server
|
|
// bypasses enforcement via IsPartOfTypeHierarchy at runtime.
|
|
string? accessRestrictions =
|
|
root.AccessRestrictions.GetAccessRestrictionsAsCode(
|
|
root.AccessRestrictionsSpecified) ??
|
|
root.DefaultAccessRestrictions.GetAccessRestrictionsAsCode(
|
|
root.DefaultAccessRestrictionsSpecified);
|
|
context.Template.AddReplacement(
|
|
Tokens.AccessRestrictionsValue,
|
|
accessRestrictions != null
|
|
? CoreUtils.Format("nodeState.AccessRestrictions = {0};", accessRestrictions)
|
|
: null);
|
|
|
|
// Role permissions
|
|
HashSet<RolePermission> rolePermissions = GetRolePermissions(root);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfRolePermissions,
|
|
NodeStateTemplates.ListOfRolePermissions,
|
|
[rolePermissions],
|
|
WriteTemplate_RolePermissions);
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_AddReference(ILoadContext context)
|
|
{
|
|
if (context.Target is not ReferenceToGenerate reference)
|
|
{
|
|
return null;
|
|
}
|
|
bool placeholder = reference.TargetNode is InstanceDesign
|
|
{
|
|
ModellingRule: ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder
|
|
};
|
|
if (placeholder)
|
|
{
|
|
context.Out.WriteLine("if (!forInstance)");
|
|
context.Out.WriteLine("{");
|
|
}
|
|
context.Out.WriteLine(
|
|
"{0}state.AddReference({1}, {2}, {3});",
|
|
placeholder ? " " : string.Empty,
|
|
m_context.ModelDesign.GetNodeIdConstant(
|
|
reference.ReferenceTypeId,
|
|
"<ReferenceType>",
|
|
kNamespaceTableContextVariable),
|
|
reference.IsInverse ? "true" : "false",
|
|
reference.TargetNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
if (placeholder)
|
|
{
|
|
context.Out.WriteLine("}");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether <see cref="GeneratorOptions.EmitNodeSetNodesOnly"/> applies:
|
|
/// the option is set and the target model was read from a NodeSet2.
|
|
/// </summary>
|
|
private bool EmitsNodeSetNodesOnly =>
|
|
m_context.Options.EmitNodeSetNodesOnly &&
|
|
m_context.ModelDesign is ModelDesignValidator { IsSourceNodeSet: true };
|
|
|
|
/// <summary>
|
|
/// With <see cref="GeneratorOptions.EmitNodeSetNodesOnly"/>, a
|
|
/// predefined node of a NodeSet2 input gets only the children the
|
|
/// NodeSet2 declares: a child the generator derives from a supertype
|
|
/// or a type definition is not added to it.
|
|
/// <para>
|
|
/// The child factories of a type are also called by the
|
|
/// <c>CreateInstanceOf*</c> factories with <c>forInstance: true</c>, so
|
|
/// in a type the omitted child is still created when its parent is
|
|
/// created for an instance; only the factories of a predefined instance
|
|
/// leave it out unconditionally.
|
|
/// </para>
|
|
/// </summary>
|
|
private bool IsOmittedFromNodeSetAddressSpace(NodeToGenerate node)
|
|
{
|
|
return EmitsNodeSetNodesOnly &&
|
|
node.IsNotExplicitlyDefined &&
|
|
GetRoot(node).InstanceOf == null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the predefined instance that <paramref name="node"/> belongs
|
|
/// to omits the undeclared nodes of its hierarchy, so that a reference
|
|
/// may only end in a node of the hierarchy that it creates. The
|
|
/// factories of a type keep their references: they are the ones the
|
|
/// <c>CreateInstanceOf*</c> factories reuse at run time.
|
|
/// </summary>
|
|
private bool OmitsUndeclaredHierarchyNodes(NodeToGenerate node)
|
|
{
|
|
return EmitsNodeSetNodesOnly &&
|
|
!node.RootIsTypeDefinition &&
|
|
GetRoot(node).InstanceOf == null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the predefined instance that <paramref name="node"/> belongs
|
|
/// to creates the node at <paramref name="path"/> of its hierarchy: the
|
|
/// node is generated and neither it nor any of its ancestors is
|
|
/// omitted (see <see cref="IsOmittedFromNodeSetAddressSpace"/>).
|
|
/// </summary>
|
|
private static bool IsCreatedInNodeSetAddressSpace(NodeToGenerate node, string path)
|
|
{
|
|
NodeToGenerate root = GetRoot(node);
|
|
if (path == root.Path)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
NodeToGenerate? target = FindDescendant(root, path);
|
|
if (target == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (NodeToGenerate current = target;
|
|
current.Parent != null;
|
|
current = current.Parent)
|
|
{
|
|
if (current.IsNotExplicitlyDefined)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static NodeToGenerate? FindDescendant(NodeToGenerate node, string path)
|
|
{
|
|
foreach (NodeToGenerate child in node.AllChildren)
|
|
{
|
|
if (child.Path == path)
|
|
{
|
|
return child;
|
|
}
|
|
NodeToGenerate? descendant = FindDescendant(child, path);
|
|
if (descendant != null)
|
|
{
|
|
return descendant;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static NodeToGenerate GetRoot(NodeToGenerate node)
|
|
{
|
|
NodeToGenerate root = node;
|
|
while (root.Parent != null)
|
|
{
|
|
root = root.Parent;
|
|
}
|
|
return root;
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_ReplaceChild(ILoadContext context)
|
|
{
|
|
if (context.Target is not NodeToGenerate node ||
|
|
node.Design is not InstanceDesign instance ||
|
|
node.Design is ViewDesign)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// A predefined instance's factories only build that instance, and a
|
|
// factory's type-template block only runs for the predefined type
|
|
// node (forInstance: false), so neither creates the omitted child.
|
|
bool omittedFromNodeSet = IsOmittedFromNodeSetAddressSpace(node);
|
|
if (omittedFromNodeSet &&
|
|
(!node.RootIsTypeDefinition ||
|
|
context.Token == Tokens.ListOfOptionalChildNodeStates))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Determine which modelling rule drives factory-method emission.
|
|
//
|
|
// For SINGLETON instance overrides (a top-level <opc:Object> in
|
|
// StandardTypes.xml whose parent has no type def), an override of
|
|
// a type-def Optional child to Mandatory must NOT force emission
|
|
// of the child when UseTypeDefinitionModellingRules is on - the
|
|
// SDK is responsible for adding only those children it actually
|
|
// implements (fixes issue #3768).
|
|
//
|
|
// The gate applies TRANSITIVELY to every Variable/Method descendant
|
|
// of a top-level singleton instance (e.g. Server,
|
|
// ServerConfiguration, HistoryServerCapabilities). Optional
|
|
// Object instances are intentionally exempt: they are still
|
|
// emitted by the singleton-specific factory with their correct
|
|
// instance-level NodeIds, because their subtrees use well-known
|
|
// descendant NodeIds (e.g.
|
|
// ServerConfiguration.CertificateGroups.DefaultHttpsGroup.
|
|
// TrustList.Open at NodeId 14095) and the SDK binds against
|
|
// them directly (e.g. ConfigurationNodeManager pairs
|
|
// CertificateGroupState by NodeId in
|
|
// <see cref="Opc.Ua.Server.ConfigurationNodeManager"/>,
|
|
// RoleStateBinding looks up
|
|
// <see cref="Opc.Ua.ObjectIds.Server_ServerCapabilities_RoleSet"/>).
|
|
// Patching every descendant NodeId from the SDK after a
|
|
// type-level factory call would require hard-coded mapping of
|
|
// dozens of well-known NodeIds per Object root and is brittle.
|
|
// Optional Object descendants' own Variable/Method children are
|
|
// still gated transitively (e.g. OperationLimits.MaxNodesPerRead
|
|
// is suppressed and the SDK programmatically adds it back via
|
|
// <see cref="Opc.Ua.Server.Diagnostics.DiagnosticsNodeManager.
|
|
// AddOperationLimitsSdkOptionalChildren"/>).
|
|
//
|
|
// For TYPE-LEVEL promotions (e.g. AnalogItemType promoting EURange
|
|
// from Optional on BaseAnalogType to Mandatory), the type-def
|
|
// tree itself is being captured and the promotion is part of the
|
|
// declared type definition, so the instance's Mandatory rule
|
|
// must be honoured even when the option is on. The synthetic
|
|
// type-instance NodeToGenerate (root.InstanceOf != null) and the
|
|
// type-def NodeToGenerate (root.RootIsTypeDefinition) are both
|
|
// excluded because IsUnderSingletonInstance is propagated only
|
|
// through the singleton-instance subtree.
|
|
bool isSingletonInstanceContext =
|
|
m_context.Options.UseTypeDefinitionModellingRules &&
|
|
node.IsUnderSingletonInstance &&
|
|
node.Parent != null &&
|
|
instance is VariableDesign or MethodDesign or ObjectDesign;
|
|
|
|
ModellingRule effectiveRule = isSingletonInstanceContext
|
|
? GetEffectiveModellingRule(node, instance)
|
|
: instance.ModellingRule;
|
|
|
|
// The "top-level non-typed parent" exception below forces children
|
|
// of an explicitly-declared singleton instance (e.g.
|
|
// <opc:Object SymbolicName="ServerConfiguration" ...> in
|
|
// StandardTypes.xml) into the always-emitted mandatory list so
|
|
// declared overrides materialize on the actual node. This
|
|
// exception must be suppressed for SINGLETON-INSTANCE roots when
|
|
// <see cref="GeneratorOptions.UseTypeDefinitionModellingRules"/>
|
|
// is on; otherwise singleton instance overrides could re-introduce
|
|
// the very Optional/unimplemented children we are trying to
|
|
// exclude from the actual instance.
|
|
//
|
|
// TYPE-DEFINITION roots (ObjectTypeDesign, VariableTypeDesign,
|
|
// MethodTypeDesign) must still emit their Optional children
|
|
// unconditionally — the type-def factory is called from many
|
|
// places (instance.Create, ConfigurationNodeManager.
|
|
// CreateNamespaceMetadataStateAsync, etc.) with
|
|
// forInstance=true, and removing them breaks deep code paths
|
|
// that rely on the type-def factory shape. Only suppress when
|
|
// the parent is itself an InstanceDesign singleton.
|
|
bool topLevelInstanceException =
|
|
node.Parent != null &&
|
|
node.Parent.Parent == null &&
|
|
node.Parent.InstanceOf == null &&
|
|
(node.Parent.Design is not InstanceDesign ||
|
|
!m_context.Options.UseTypeDefinitionModellingRules);
|
|
|
|
// Explicitly-declared descendants of a concrete predefined instance
|
|
// (a user-modelled <opc:Object>/<opc:Variable> declared under
|
|
// Objects, e.g. a device instance) must be materialised on the
|
|
// actual instance regardless of their (possibly type-inherited)
|
|
// modelling rule: they are real nodes present in the model, not
|
|
// modelling-rule-driven optional children the SDK adds on demand.
|
|
// The topLevelInstanceException above only covers the instance's
|
|
// DIRECT children (node.Parent.Parent == null); deeper descendants
|
|
// — e.g. the EURange / EngineeringUnits properties of a
|
|
// BaseAnalogType / AnalogUnitType variable nested under the
|
|
// instance (issue #3964) — inherit IsUnderSingletonInstance and
|
|
// would otherwise land in the `if (!forInstance)` type-template
|
|
// block and be dropped from the instance. SDK-managed singletons
|
|
// (Server, ServerConfiguration, …) are generated with
|
|
// UseTypeDefinitionModellingRules on and keep the #3768
|
|
// optional-child suppression, so they are excluded here.
|
|
bool concreteInstanceDescendant =
|
|
node.IsUnderSingletonInstance &&
|
|
!node.RootIsTypeDefinition &&
|
|
node.InstanceOf == null &&
|
|
!node.IsNotExplicitlyDefined &&
|
|
!m_context.Options.UseTypeDefinitionModellingRules;
|
|
|
|
bool alwaysEmitInstanceChild =
|
|
topLevelInstanceException || concreteInstanceDescendant;
|
|
|
|
if (context.Token == Tokens.ListOfOptionalChildNodeStates)
|
|
{
|
|
if (effectiveRule == ModellingRule.Mandatory)
|
|
{
|
|
return null;
|
|
}
|
|
// Real instance children of a top-level (non-typed) parent belong in
|
|
// the always-emitted list so they materialize for the actual instance,
|
|
// not only when the node is treated as a type template.
|
|
if (alwaysEmitInstanceChild)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Otherwise only add mandatory children - all others are created on demand
|
|
else if (effectiveRule != ModellingRule.Mandatory)
|
|
{
|
|
// Exception: real instance children of a top-level (non-typed) parent.
|
|
if (!alwaysEmitInstanceChild)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
string forInstanceVariableValue =
|
|
node.RootIsTypeDefinition ? "forInstance" :
|
|
node.Parent?.InstanceOf != null || node.Parent?.Parent == null ? "true" : "forInstance";
|
|
if (!omittedFromNodeSet)
|
|
{
|
|
EmitChildFactoryCall(
|
|
context.Out,
|
|
node,
|
|
instance,
|
|
effectiveRule,
|
|
forInstanceVariableValue);
|
|
return null;
|
|
}
|
|
|
|
// An omitted child of a type is created for an instance only.
|
|
context.Out.WriteLine("if (forInstance)");
|
|
context.Out.WriteLine("{");
|
|
EmitChildFactoryCall(
|
|
new IndentedTemplateWriter(context.Out),
|
|
node,
|
|
instance,
|
|
effectiveRule,
|
|
forInstanceVariableValue);
|
|
context.Out.WriteLine("}");
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the call that creates <paramref name="node"/> and adds it to
|
|
/// the <c>state</c> of its parent's factory.
|
|
/// </summary>
|
|
private void EmitChildFactoryCall(
|
|
ITemplateWriter output,
|
|
NodeToGenerate node,
|
|
InstanceDesign instance,
|
|
ModellingRule effectiveRule,
|
|
string forInstanceVariableValue)
|
|
{
|
|
if (node.Parent != null && IsInAddressSpace(node.Parent))
|
|
{
|
|
switch (node.Parent.Design)
|
|
{
|
|
case TypeDesign:
|
|
output.WriteLine(
|
|
"state.AddChild(Create{0}(context, state, forInstance: {1}));",
|
|
instance.SymbolicId.Name,
|
|
forInstanceVariableValue);
|
|
return;
|
|
case InstanceDesign parentInstance:
|
|
if (HasFixedChildSlot(parentInstance.TypeDefinitionNode, instance.SymbolicName.Name) ||
|
|
IsBuiltInProperty(node))
|
|
{
|
|
// Children whose SymbolicName ends in "_Placeholder"
|
|
// come from OptionalPlaceholder/MandatoryPlaceholder
|
|
// slots on the type, which emit Add*_Placeholder
|
|
// methods (cardinality 0..N) instead of
|
|
// CreateOrReplace*_Placeholder methods (which only
|
|
// exist for fixed Mandatory/Optional slots).
|
|
// Fall through to AddChild for these.
|
|
bool isPlaceholderSlot = instance.SymbolicName.Name
|
|
.EndsWith("_Placeholder", StringComparison.Ordinal);
|
|
if (!isPlaceholderSlot)
|
|
{
|
|
switch (effectiveRule)
|
|
{
|
|
case ModellingRule.Mandatory:
|
|
case ModellingRule.Optional:
|
|
EmitCreateOrReplaceChild(
|
|
output,
|
|
node,
|
|
instance,
|
|
forInstanceVariableValue);
|
|
return;
|
|
case ModellingRule.OptionalPlaceholder:
|
|
case ModellingRule.MandatoryPlaceholder:
|
|
// TODO
|
|
break;
|
|
case ModellingRule.ExposesItsArray:
|
|
case ModellingRule.None:
|
|
case ModellingRule.CardinalityRestriction:
|
|
case ModellingRule.MandatoryShared:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
output.WriteLine(
|
|
"state.AddChild(Create{0}(context, state, forInstance: {1}));",
|
|
instance.SymbolicId.Name,
|
|
forInstanceVariableValue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the <c>state.CreateOrReplace{Name}(context, Create{Symbolic}(context, state, forInstance: …))</c>
|
|
/// call site for a child of an InstanceDesign parent. When the child
|
|
/// lives in a type-level factory body (<see cref="NodeToGenerate.RootIsTypeDefinition"/>
|
|
/// is <c>true</c>) AND the type has well-known singleton instances
|
|
/// whose corresponding child factories have been collected, also
|
|
/// dispatches the <c>forInstance: true</c> path into the appropriate
|
|
/// singleton-instance child factory. This ensures that lazy-added
|
|
/// methods on singleton owners (e.g.
|
|
/// <c>serverObject.AddGetMonitoredItems(context, MethodIds.Server_GetMonitoredItems)</c>)
|
|
/// produce synthesized children (<c>InputArguments</c>,
|
|
/// <c>OutputArguments</c>) at their well-known instance NodeIds
|
|
/// rather than the type-level placeholders.
|
|
/// </summary>
|
|
private void EmitCreateOrReplaceChild(
|
|
ITemplateWriter output,
|
|
NodeToGenerate node,
|
|
InstanceDesign instance,
|
|
string forInstanceVariableValue)
|
|
{
|
|
List<
|
|
(
|
|
NodeToGenerate Singleton,
|
|
NodeToGenerate SingletonChild,
|
|
string SingletonChildSymbolicId)> singletonChildren = FindSingletonChildren(node);
|
|
if (singletonChildren.Count == 0)
|
|
{
|
|
output.WriteLine(
|
|
"state.CreateOrReplace{0}(context, Create{1}(context, state, " +
|
|
"forInstance: {2}), assignInstanceNodeIds: false);",
|
|
instance.SymbolicName.Name,
|
|
instance.SymbolicId.Name,
|
|
forInstanceVariableValue);
|
|
return;
|
|
}
|
|
|
|
// Dispatch on the parent's NodeId so the singleton-instance
|
|
// child factory is invoked when the type-level factory is
|
|
// materialising an actual well-known instance subtree. The
|
|
// <paramref name="forInstance"/> gate keeps the type-template
|
|
// emission path (forInstance: false) unchanged.
|
|
output.WriteLine("if ({0} && parent != null)", forInstanceVariableValue);
|
|
output.WriteLine("{");
|
|
for (int i = 0; i < singletonChildren.Count; i++)
|
|
{
|
|
(NodeToGenerate singletonRoot, NodeToGenerate _, string singletonChildSymbolicId)
|
|
= singletonChildren[i];
|
|
string keyword = i == 0 ? "if" : "else if";
|
|
output.WriteLine(
|
|
" {0} (parent.NodeId.Equals({1}))",
|
|
keyword,
|
|
singletonRoot.Design.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
output.WriteLine(" {");
|
|
output.WriteLine(
|
|
" state.CreateOrReplace{0}(context, Create{1}(context, state, " +
|
|
"forInstance: true), assignInstanceNodeIds: false);",
|
|
instance.SymbolicName.Name,
|
|
singletonChildSymbolicId);
|
|
output.WriteLine(" }");
|
|
}
|
|
output.WriteLine(" else");
|
|
output.WriteLine(" {");
|
|
output.WriteLine(
|
|
" state.CreateOrReplace{0}(context, Create{1}(context, state, " +
|
|
"forInstance: true), assignInstanceNodeIds: false);",
|
|
instance.SymbolicName.Name,
|
|
instance.SymbolicId.Name);
|
|
output.WriteLine(" }");
|
|
output.WriteLine("}");
|
|
output.WriteLine("else");
|
|
output.WriteLine("{");
|
|
output.WriteLine(
|
|
" state.CreateOrReplace{0}(context, Create{1}(context, state, " +
|
|
"forInstance: {2}), assignInstanceNodeIds: false);",
|
|
instance.SymbolicName.Name,
|
|
instance.SymbolicId.Name,
|
|
forInstanceVariableValue);
|
|
output.WriteLine("}");
|
|
}
|
|
|
|
private bool WriteTemplate_RolePermissions(IWriteContext context)
|
|
{
|
|
if (context.Target is not HashSet<RolePermission> rolePermissions ||
|
|
rolePermissions.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfRolePermissions,
|
|
NodeStateTemplates.RolePermission,
|
|
rolePermissions,
|
|
LoadTemplate_RolePermissionEntry,
|
|
WriteTemplate_RolePermissionEntry);
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private bool WriteTemplate_ArgumentCollection(IWriteContext context)
|
|
{
|
|
switch (context.Target)
|
|
{
|
|
case IList<Argument> arguments:
|
|
// The parameter that is packed into the Argument value
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfValues,
|
|
NodeStateTemplates.ArgumentValue,
|
|
arguments,
|
|
WriteTemplate_Argument);
|
|
break;
|
|
case Parameter[] parameters:
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfValues,
|
|
NodeStateTemplates.ArgumentValue,
|
|
parameters,
|
|
WriteTemplate_Argument);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(Tokens.DataType, "global::Opc.Ua.Argument");
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private bool WriteTemplate_Argument(IWriteContext context)
|
|
{
|
|
Argument? argument = null;
|
|
Parameter? parameter;
|
|
switch (context.Target)
|
|
{
|
|
case Argument arg:
|
|
argument = arg;
|
|
parameter = argument.Value as Parameter;
|
|
break;
|
|
case Parameter parm:
|
|
parameter = parm;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.Name,
|
|
parameter == null ?
|
|
argument!.Name.AsStringLiteral() :
|
|
parameter.Name.AsStringLiteral());
|
|
context.Template.AddReplacement(
|
|
Tokens.DataType,
|
|
m_context.ModelDesign.GetNodeIdConstant(
|
|
parameter?.DataType,
|
|
"<DataType>",
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueRank,
|
|
parameter?.ValueRank.GetValueRankAsCode(parameter.ArrayDimensions));
|
|
context.Template.AddReplacement(
|
|
Tokens.ArrayDimensions,
|
|
parameter?.ValueRank.GetArrayDimensionsAsCode(parameter.ArrayDimensions) ?? "default");
|
|
context.Template.AddReplacement(
|
|
Tokens.Description,
|
|
(parameter?.Description).GetLocalizedTextAsCode(true));
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private bool WriteTemplate_MethodArguments(IWriteContext context)
|
|
{
|
|
if (context.Target is not MethodArgumentsToGenerate arguments)
|
|
{
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(
|
|
Tokens.PropertyName,
|
|
arguments.PropertyName);
|
|
context.Template.AddReplacement(
|
|
Tokens.BrowseName,
|
|
arguments.BrowseName);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfValues,
|
|
NodeStateTemplates.ArgumentValue,
|
|
arguments.Arguments,
|
|
WriteTemplate_Argument);
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private TemplateString? LoadTemplate_RolePermissionEntry(ILoadContext context)
|
|
{
|
|
if (context.Target is not RolePermission)
|
|
{
|
|
return null;
|
|
}
|
|
return NodeStateTemplates.RolePermission;
|
|
}
|
|
|
|
private bool WriteTemplate_RolePermissionEntry(IWriteContext context)
|
|
{
|
|
if (context.Target is not RolePermission rolePermission)
|
|
{
|
|
return false;
|
|
}
|
|
if (!m_context.ModelDesign.TryFindNode(
|
|
rolePermission.Role,
|
|
rolePermission.Role.Name,
|
|
"RoleType",
|
|
out ObjectDesign? roleNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.RoleIdConstant,
|
|
roleNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.PermissionsValue,
|
|
CoreUtils.Format(
|
|
"(uint)({0})",
|
|
rolePermission.Permission.GetPermissionTypeAsCode()));
|
|
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private void AddNodeStateClassVariableTypeReplacements(
|
|
IWriteContext context,
|
|
VariableTypeDesign variableType)
|
|
{
|
|
AddNodeStateClassTypeReplacements(context, variableType);
|
|
BasicDataType basicType = variableType.DataTypeNode!.BasicDataType;
|
|
|
|
if (variableType.SymbolicName.Name == "TwoStateDiscreteType")
|
|
|
|
{
|
|
variableType.ValueRank = ValueRank.Scalar;
|
|
}
|
|
if (!variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank))
|
|
{
|
|
// Overrides a variable type that does not require a template parameter so we
|
|
// need to add it
|
|
context.Template.AddReplacement(Tokens.BaseT, string.Empty);
|
|
}
|
|
else
|
|
{
|
|
// Overrides a typed variable type where the base class requires a template
|
|
// parameter, so the same template parameter is needed plus the Variant builder
|
|
// to access the variant content of the type T.
|
|
string parameter = GetTemplateParameter(variableType, out string variantBuilder);
|
|
if (parameter == "<T>" && variableType.ValueRank != ValueRank.Scalar)
|
|
{
|
|
parameter = "<global::Opc.Ua.Variant>";
|
|
variantBuilder = "global::Opc.Ua.VariantBuilder";
|
|
}
|
|
if (string.IsNullOrEmpty(parameter))
|
|
{
|
|
context.Template.AddReplacement(Tokens.BaseT, string.Empty);
|
|
}
|
|
else
|
|
{
|
|
// The class extends from the generic one through its inner
|
|
// Implementation<TBuilder> class to provide generic variant
|
|
// value accessors
|
|
context.Template.AddReplacement(
|
|
Tokens.BaseT,
|
|
CoreUtils.Format("{0}.Implementation<{1}>", parameter, variantBuilder));
|
|
}
|
|
}
|
|
|
|
string valueRank = variableType.ValueRank.GetValueRankAsCode(
|
|
variableType.ArrayDimensions);
|
|
|
|
if (variableType.ValueRank == ValueRank.ScalarOrArray)
|
|
{
|
|
for (TypeDesign? baseType = variableType.BaseTypeNode;
|
|
baseType != null;
|
|
baseType = baseType.BaseTypeNode)
|
|
{
|
|
if (baseType.SymbolicId ==
|
|
new XmlQualifiedName("DataItemType", Namespaces.OpcUa))
|
|
{
|
|
valueRank = $"global::Opc.Ua.ValueRanks.{ValueRank.Scalar}";
|
|
}
|
|
}
|
|
}
|
|
|
|
AddVariantAccessor(context, variableType.DataTypeNode);
|
|
|
|
if (variableType.DataTypeNode.NeedsCloning())
|
|
{
|
|
// Must be the fully qualified type name: the cast lands in the
|
|
// generated VariableTypeValue class, whose enclosing namespace is
|
|
// not the one declaring the data type for a cross-model structure.
|
|
// The same name the value field is declared with.
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueWrite,
|
|
CoreUtils.Format(
|
|
"CopyOnWrite ? ({0})global::Opc.Ua.CoreUtils.Clone(newValue) : newValue",
|
|
variableType.DataTypeNode.GetDotNetTypeName(
|
|
ValueRank.Scalar,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
nullable: NullableAnnotation.NonNullable)));
|
|
}
|
|
else
|
|
{
|
|
context.Template.AddReplacement(Tokens.ValueWrite, "newValue");
|
|
}
|
|
if (variableType.DataTypeNode.IsDotNetEqualityComparable(variableType.ValueRank))
|
|
{
|
|
context.Template.AddReplacement(Tokens.ValueComparison, "m_value != newValue");
|
|
}
|
|
else
|
|
{
|
|
context.Template.AddReplacement(Tokens.ValueComparison,
|
|
"!global::Opc.Ua.CoreUtils.IsEqual(m_value, newValue)");
|
|
}
|
|
// The VariableType default value is emitted by the type's factory
|
|
// (AddVariableTypeStateFactoryReplacements), which maps namespace
|
|
// indexes through the NodeSet's table (OPC 10000-6 F.14). The state
|
|
// class template has no default value: computing one here without
|
|
// that table produced a NodeSet-local literal nobody rendered and
|
|
// registered an unused XML value resource.
|
|
context.Template.AddReplacement(Tokens.ValueRank, valueRank);
|
|
context.Template.AddReplacement(
|
|
Tokens.ArrayDimensions,
|
|
variableType.ValueRank.GetArrayDimensionsAsCode(
|
|
variableType.ArrayDimensions) ??
|
|
"default");
|
|
context.Template.AddReplacement(Tokens.IsAbstract, variableType.IsAbstract);
|
|
context.Template.AddReplacement(
|
|
Tokens.AccessLevelValue,
|
|
variableType.AccessLevel.GetAccessLevelAsCode());
|
|
context.Template.AddReplacement(
|
|
Tokens.MinimumSamplingIntervalValue,
|
|
variableType.GetMinimumSamplingIntervalAsCode());
|
|
context.Template.AddReplacement(
|
|
Tokens.Historizing,
|
|
variableType.Historizing);
|
|
context.Template.AddReplacement(
|
|
Tokens.DataType,
|
|
variableType.DataTypeNode.SymbolicName.Name);
|
|
context.Template.AddReplacement(
|
|
Tokens.DataTypeNamespacePrefix,
|
|
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
|
|
variableType.DataTypeNode.SymbolicId.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.DataTypeNamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
|
|
variableType.DataTypeNode.SymbolicId.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.TypedVariableType,
|
|
NodeStateTemplates.VariableTypeWithTypedValue_Class,
|
|
[variableType],
|
|
LoadTemplate_TypedVariableType,
|
|
WriteTemplate_TypedVariableType);
|
|
context.Template.AddReplacement(
|
|
Tokens.VariableTypeValue,
|
|
NodeStateTemplates.VariableTypeValue_Class,
|
|
[variableType],
|
|
LoadTemplate_VariableTypeValue,
|
|
WriteTemplate_VariableTypeValue);
|
|
}
|
|
|
|
private void AddNodeStateClassObjectTypeReplacements(
|
|
IWriteContext context,
|
|
ObjectTypeDesign objectType)
|
|
{
|
|
AddNodeStateClassTypeReplacements(context, objectType);
|
|
context.Template.AddReplacement(Tokens.BaseT, string.Empty);
|
|
context.Template.AddReplacement(Tokens.IsAbstract, objectType.IsAbstract);
|
|
context.Template.AddReplacement(Tokens.EventNotifier,
|
|
objectType.GetEventNotifierAsCode());
|
|
context.Template.AddReplacement(
|
|
Tokens.ElementNamespaceOverride,
|
|
BuildElementNamespaceOverride(objectType));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the <c>ElementNamespaceUri</c> override for
|
|
/// <c>FiniteStateMachineType</c> subtypes. Part 5 qualifies the
|
|
/// numeric state and transition NodeIds a machine reports in
|
|
/// <c>CurrentState/Id</c> and <c>LastTransition/Id</c> with the
|
|
/// namespace of the model that declares those elements — which
|
|
/// is not necessarily the machine's own namespace: a subtype
|
|
/// that adds no states of its own keeps reporting the base
|
|
/// type's, and the base class default (the OPC UA namespace) is
|
|
/// then already right. Returns <c>null</c> (collapsing the
|
|
/// template line) for every other object type.
|
|
/// </summary>
|
|
private string? BuildElementNamespaceOverride(ObjectTypeDesign objectType)
|
|
{
|
|
if (!IsFiniteStateMachineSubtype(objectType))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string? namespaceUri = FindElementNamespaceUri(objectType);
|
|
if (string.IsNullOrEmpty(namespaceUri) ||
|
|
namespaceUri == Namespaces.OpcUa)
|
|
{
|
|
// The base class already returns the OPC UA namespace.
|
|
return null;
|
|
}
|
|
|
|
string? constant;
|
|
try
|
|
{
|
|
constant = m_context.ModelDesign.Namespaces
|
|
.GetConstantSymbolForNamespace(namespaceUri);
|
|
}
|
|
catch (ArgumentException e)
|
|
{
|
|
// A namespace entry with a URI but no Name — surface it
|
|
// instead of failing the whole generation for a
|
|
// diagnostic-only feature.
|
|
m_logger.LogWarning(
|
|
"Cannot emit ElementNamespaceUri for {Type}: {Error}",
|
|
objectType.SymbolicName?.Name,
|
|
e.Message);
|
|
return null;
|
|
}
|
|
if (string.IsNullOrEmpty(constant))
|
|
{
|
|
// Failing open here means the machine keeps the base
|
|
// default (the OPC UA namespace) and its element ids
|
|
// land in namespace 0 — say so rather than being silent.
|
|
m_logger.LogWarning(
|
|
"No namespace constant for '{Uri}' — {Type} will " +
|
|
"qualify its state and transition ids with the OPC " +
|
|
"UA namespace. Add the declaring model to the " +
|
|
"design's <Namespaces> to fix this.",
|
|
namespaceUri,
|
|
objectType.SymbolicName?.Name);
|
|
return null;
|
|
}
|
|
|
|
return CoreUtils.Format(
|
|
"/// <inheritdoc/>{0}" +
|
|
" protected override string ElementNamespaceUri => {1};",
|
|
Environment.NewLine,
|
|
constant);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The namespace of the nearest type in the hierarchy that
|
|
/// actually declares <c>StateType</c> / <c>TransitionType</c>
|
|
/// children. A subtype that only adds behaviour inherits its
|
|
/// elements — and therefore their namespace — from its base.
|
|
/// </summary>
|
|
internal static string? FindElementNamespaceUri(ObjectTypeDesign objectType)
|
|
{
|
|
TypeDesign? current = objectType;
|
|
while (current != null)
|
|
{
|
|
if (DeclaresStateMachineElements(current))
|
|
{
|
|
return current.SymbolicName?.Namespace;
|
|
}
|
|
current = current.BaseTypeNode;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
internal static bool DeclaresStateMachineElements(TypeDesign type)
|
|
{
|
|
InstanceDesign[]? children = type.Children?.Items;
|
|
if (children == null)
|
|
{
|
|
return false;
|
|
}
|
|
foreach (InstanceDesign child in children)
|
|
{
|
|
if (child is not ObjectDesign childObject ||
|
|
childObject.TypeDefinition == null ||
|
|
!string.Equals(
|
|
childObject.TypeDefinition.Namespace,
|
|
Namespaces.OpcUa,
|
|
StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
if (childObject.OveriddenNode != null)
|
|
{
|
|
// Re-declaring an inherited state (to attach a
|
|
// description, a modelling rule, ...) does not make
|
|
// this type the one that declares the elements —
|
|
// the ids still live in the base model's namespace.
|
|
continue;
|
|
}
|
|
string typeDefName = childObject.TypeDefinition.Name;
|
|
if (string.Equals(typeDefName, kStateTypeName, StringComparison.Ordinal) ||
|
|
string.Equals(typeDefName, kInitialStateTypeName, StringComparison.Ordinal) ||
|
|
string.Equals(typeDefName, kTransitionTypeName, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the type derives, however indirectly, from the
|
|
/// standard <c>FiniteStateMachineType</c>.
|
|
/// </summary>
|
|
internal static bool IsFiniteStateMachineSubtype(ObjectTypeDesign objectType)
|
|
{
|
|
TypeDesign? current = objectType;
|
|
while (current != null)
|
|
{
|
|
if (string.Equals(
|
|
current.SymbolicName?.Name,
|
|
kFiniteStateMachineTypeName,
|
|
StringComparison.Ordinal) &&
|
|
string.Equals(
|
|
current.SymbolicName?.Namespace,
|
|
Namespaces.OpcUa,
|
|
StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
current = current.BaseTypeNode;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private const string kFiniteStateMachineTypeName = "FiniteStateMachineType";
|
|
private const string kStateTypeName = "StateType";
|
|
private const string kInitialStateTypeName = "InitialStateType";
|
|
private const string kTransitionTypeName = "TransitionType";
|
|
|
|
private void AddNodeStateClassMethodTypeReplacements(
|
|
IWriteContext context,
|
|
MethodDesign method)
|
|
{
|
|
(Parameter[] inputArguments, Parameter[] outputArguments) =
|
|
MethodDesignArgumentResolver.ResolveMethodArguments(method);
|
|
string targetNamespace = m_context.ModelDesign.TargetNamespace.Value;
|
|
string declaredClassName = method.GetNodeStateClassName(
|
|
targetNamespace,
|
|
[],
|
|
applyStandardFallback: false);
|
|
ModelDesignExtensions.AssignMethodArgumentCodeNames(
|
|
inputArguments,
|
|
outputArguments,
|
|
s_methodArgumentCodeNameScope,
|
|
declaredClassName + "Result");
|
|
// Declarations require an unqualified identifier, while fallback
|
|
// tracking requires the fully-qualified name. Neither may apply
|
|
// the reference-only fallback policy.
|
|
StandardMethodStateFallback.RecordDeclaredMethodState(
|
|
method.GetNodeStateClassName(
|
|
targetNamespace,
|
|
m_context.ModelDesign.Namespaces,
|
|
applyStandardFallback: false));
|
|
context.Template.AddReplacement(
|
|
Tokens.ClassName,
|
|
declaredClassName);
|
|
context.Template.AddReplacement(
|
|
Tokens.OwnerClassName,
|
|
declaredClassName);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfInputArguments,
|
|
inputArguments,
|
|
LoadTemplate_ListOfInputArguments);
|
|
context.Template.AddReplacement(
|
|
Tokens.OnCallDeclaration,
|
|
[method],
|
|
LoadTemplate_OnCallDeclaration);
|
|
context.Template.AddReplacement(
|
|
Tokens.OnCallAsyncDeclaration,
|
|
[method],
|
|
LoadTemplate_OnCallAsyncDeclaration);
|
|
context.Template.AddReplacement(
|
|
Tokens.OnCallImplementation,
|
|
[method],
|
|
LoadTemplate_OnCallImplementation);
|
|
context.Template.AddReplacement(
|
|
Tokens.OnCallAsyncImplementation,
|
|
[method],
|
|
LoadTemplate_OnCallAsyncImplementation);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfOutputDeclarations,
|
|
outputArguments,
|
|
LoadTemplate_ListOfOutputDeclarations);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfOutputArgumentsFromResult,
|
|
outputArguments,
|
|
LoadTemplate_ListOfOutputArgumentsFromResult);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfOutputArguments,
|
|
outputArguments,
|
|
LoadTemplate_ListOfOutputArguments);
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfResultProperties,
|
|
outputArguments,
|
|
LoadTemplate_ListOfResultProperties);
|
|
}
|
|
|
|
private void AddNodeStateClassTypeReplacements(
|
|
IWriteContext context,
|
|
TypeDesign type)
|
|
{
|
|
context.Template.AddReplacement(Tokens.ClassName, type.ClassName);
|
|
// OwnerClassName mirrors ClassName at the type scope so per-child
|
|
// sub-templates (e.g. NodeStateTemplates.OptionalMethod) can refer
|
|
// to the parent (owner) class when generating Add{Child} methods
|
|
// that return `this` for chaining. The per-child WriteTemplate
|
|
// overrides Tokens.ClassName but leaves OwnerClassName untouched,
|
|
// and the template engine cascades to this outer template when
|
|
// OwnerClassName is not found locally. The class declaration
|
|
// template appends "State" to ClassName (see
|
|
// NodeStateClassObjectType / NodeStateClassVariableType), so do
|
|
// the same here so the return type matches the declared class.
|
|
context.Template.AddReplacement(
|
|
Tokens.OwnerClassName,
|
|
type.ClassName + "State");
|
|
context.Template.AddReplacement(
|
|
Tokens.BaseClassName,
|
|
type.BaseTypeNode!.GetClassName(m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(
|
|
Tokens.BaseTypeNamespacePrefix,
|
|
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
|
|
type.BaseTypeNode!.SymbolicId.Namespace));
|
|
context.Template.AddReplacement(
|
|
Tokens.BaseTypeNamespaceUri,
|
|
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
|
|
type.BaseTypeNode.SymbolicId.Namespace));
|
|
}
|
|
|
|
private void AddObjectTypeStateFactoryReplacements(
|
|
IWriteContext context,
|
|
ObjectTypeDesign node)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.SuperTypeId,
|
|
node.BaseTypeNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassName,
|
|
"global::Opc.Ua.BaseObjectTypeState");
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassFactory,
|
|
"new global::Opc.Ua.BaseObjectTypeState");
|
|
}
|
|
|
|
private void AddVariableTypeStateFactoryReplacements(
|
|
IWriteContext context,
|
|
VariableTypeDesign node)
|
|
{
|
|
context.Template.AddReplacement(Tokens.SuperTypeId,
|
|
node.BaseTypeNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassName,
|
|
"global::Opc.Ua.BaseDataVariableTypeState");
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassFactory,
|
|
"new global::Opc.Ua.BaseDataVariableTypeState");
|
|
|
|
context.Template.AddReplacement(Tokens.ValueCode, CoreUtils.Format(
|
|
"baseState.WrappedValue = {0};",
|
|
node.DataTypeNode!.GetValueAsCode(
|
|
node.ValueRank,
|
|
node.DefaultValue,
|
|
node.DecodedValue,
|
|
true,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
m_messageContext,
|
|
() => AddXmlInitializerForComplexValue(
|
|
node,
|
|
node.DataTypeNode!,
|
|
node.DefaultValue)!,
|
|
node.DecodedValueNamespaceUris,
|
|
kNamespaceTableContextVariable)));
|
|
string dataTypeId =
|
|
GetNodeIdConstantForDataType(node, m_context.ModelDesign.Namespaces);
|
|
context.Template.AddReplacement(
|
|
Tokens.DataTypeIdConstant,
|
|
dataTypeId ?? "global::Opc.Ua.NodeId.Null");
|
|
string valueRank = node.ValueRank.GetValueRankAsCode(
|
|
node.ArrayDimensions);
|
|
context.Template.AddReplacement(Tokens.ValueRank, valueRank);
|
|
string? arrayDims = node.ValueRank.GetArrayDimensionsAsCode(
|
|
node.ArrayDimensions);
|
|
context.Template.AddReplacement(
|
|
Tokens.ArrayDimensions,
|
|
!string.IsNullOrEmpty(arrayDims)
|
|
? CoreUtils.Format("baseState.ArrayDimensions = {0};", arrayDims)
|
|
: null);
|
|
}
|
|
|
|
private void AddVariableStateFactoryReplacements(
|
|
IWriteContext context,
|
|
NodeToGenerate nodeToGenerate,
|
|
VariableDesign node,
|
|
HashSet<ReferenceToGenerate> references)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassName,
|
|
node.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassFactory,
|
|
node.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
asFactory: true));
|
|
context.Template.AddReplacement(
|
|
Tokens.TypeDefinitionId,
|
|
node.TypeDefinitionNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.ReferenceTypeId,
|
|
m_context.ModelDesign.GetNodeIdConstant(
|
|
node.ReferenceType,
|
|
"<ReferenceType>",
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.ModellingRuleId,
|
|
GetModellingRuleReplacement(
|
|
GetEffectiveModellingRule(nodeToGenerate, node)));
|
|
context.Template.AddReplacement(
|
|
Tokens.DataTypeIdConstant,
|
|
GetNodeIdConstantForDataType(node, m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueRank,
|
|
node.ValueRank.GetValueRankAsCode(node.ArrayDimensions));
|
|
|
|
string? arrayDims = node.ValueRank.GetArrayDimensionsAsCode(node.ArrayDimensions);
|
|
context.Template.AddReplacement(
|
|
Tokens.ArrayDimensions,
|
|
!string.IsNullOrEmpty(arrayDims)
|
|
? CoreUtils.Format("baseState.ArrayDimensions = {0};", arrayDims)
|
|
: null);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.AccessLevelValue,
|
|
node.GetAccessLevelAsCode());
|
|
context.Template.AddReplacement(
|
|
Tokens.UserAccessLevelValue,
|
|
node.GetUserAccessLevelAsCode());
|
|
context.Template.AddReplacement(
|
|
Tokens.MinimumSamplingIntervalValue,
|
|
node.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture));
|
|
context.Template.AddReplacement(
|
|
Tokens.HistorizingValue,
|
|
node.Historizing);
|
|
|
|
// set dictionary to point to embedded schemas
|
|
if (node.TypeDefinitionNode!.SymbolicId ==
|
|
new XmlQualifiedName("DataTypeDictionaryType", Namespaces.OpcUa))
|
|
{
|
|
NodeDesign? typeSystemReference = references.FirstOrDefault()?.TargetNode;
|
|
switch (typeSystemReference?.SymbolicId.Name)
|
|
{
|
|
case "XmlSchema_TypeSystem":
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueCode,
|
|
"baseState.WrappedValue = global::Opc.Ua.Variant.From(" +
|
|
"global::Opc.Ua.ByteString.From(XmlSchemas.TypesXsd.ToArray()));");
|
|
return;
|
|
case "OPCBinarySchema_TypeSystem":
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueCode,
|
|
"baseState.WrappedValue = global::Opc.Ua.Variant.From(" +
|
|
"global::Opc.Ua.ByteString.From(XmlSchemas.TypesBsd.ToArray()));");
|
|
return;
|
|
}
|
|
// unknown type system
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueCode,
|
|
"baseState.WrappedValue = global::Opc.Ua.Variant.Null;");
|
|
return;
|
|
}
|
|
|
|
Parameter[]? methodArguments = GetMethodArgumentParameters(nodeToGenerate);
|
|
if (methodArguments != null)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueCode,
|
|
NodeStateTemplates.VariantArrayOfValue,
|
|
[methodArguments],
|
|
WriteTemplate_ArgumentCollection);
|
|
}
|
|
else if (node.DecodedValue is IList<Argument> args)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueCode,
|
|
NodeStateTemplates.VariantArrayOfValue,
|
|
[args],
|
|
WriteTemplate_ArgumentCollection);
|
|
}
|
|
else if (node.DecodedValue is ArrayOf<Argument> arguments)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.ValueCode,
|
|
NodeStateTemplates.VariantArrayOfValue,
|
|
[arguments.ToList()],
|
|
WriteTemplate_ArgumentCollection);
|
|
}
|
|
else
|
|
{
|
|
context.Template.AddReplacement(Tokens.ValueCode, CoreUtils.Format(
|
|
"baseState.WrappedValue = {0};",
|
|
node.DataTypeNode!.GetValueAsCode(
|
|
node.ValueRank,
|
|
node.DefaultValue,
|
|
node.DecodedValue,
|
|
true,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
m_messageContext,
|
|
() => AddXmlInitializerForComplexValue(
|
|
node,
|
|
node.DataTypeNode!,
|
|
node.DefaultValue)!,
|
|
node.DecodedValueNamespaceUris,
|
|
kNamespaceTableContextVariable)));
|
|
}
|
|
}
|
|
|
|
private static Parameter[]? GetMethodArgumentParameters(NodeToGenerate node)
|
|
{
|
|
if (node.Parent?.Design is not MethodDesign method)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (node.Design!.SymbolicName == s_inputArgumentsBrowseName)
|
|
{
|
|
return MethodDesignArgumentResolver.ResolveMethodInputs(method);
|
|
}
|
|
|
|
if (node.Design.SymbolicName == s_outputArgumentsBrowseName)
|
|
{
|
|
return MethodDesignArgumentResolver.ResolveMethodOutputs(method);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void AddReferenceTypeStateFactoryReplacements(
|
|
IWriteContext context,
|
|
ReferenceTypeDesign node)
|
|
{
|
|
context.Template.AddReplacement(Tokens.SuperTypeId,
|
|
node.BaseTypeNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
|
|
context.Template.AddReplacement(Tokens.SymmetricValue, node.Symmetric);
|
|
context.Template.AddReplacement(Tokens.InverseNameValue, GetInverseNameValue(node));
|
|
}
|
|
|
|
private void AddDataTypeStateFactoryReplacements(
|
|
IWriteContext context,
|
|
DataTypeDesign node)
|
|
{
|
|
context.Template.AddReplacement(Tokens.SuperTypeId,
|
|
node.BaseTypeNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
|
|
context.Template.AddReplacement(Tokens.Purpose,
|
|
(Export.DataTypePurpose)(int)(node.Purpose == DataTypePurpose.Testing ?
|
|
DataTypePurpose.CodeGenerator :
|
|
node.Purpose));
|
|
|
|
if (node.BasicDataType is not BasicDataType.Enumeration and not BasicDataType.UserDefined)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.DataTypeDefinition,
|
|
"global::Opc.Ua.ExtensionObject.Null");
|
|
}
|
|
else
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.DataTypeDefinition,
|
|
CoreUtils.Format(
|
|
"new global::Opc.Ua.ExtensionObject(global::{0}.DataTypeDefinitions.Create{1}({2}))",
|
|
m_context.ModelDesign.Namespaces.GetNamespacePrefix(node.SymbolicName.Namespace),
|
|
node.SymbolicName.Name,
|
|
kNamespaceTableContextVariable));
|
|
}
|
|
}
|
|
|
|
private void AddObjectReplacements(
|
|
IWriteContext context,
|
|
NodeToGenerate nodeToGenerate,
|
|
ObjectDesign node,
|
|
HashSet<ReferenceToGenerate> references)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassName,
|
|
node.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassFactory,
|
|
node.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
asFactory: true));
|
|
context.Template.AddReplacement(
|
|
Tokens.TypeDefinitionId,
|
|
node.TypeDefinitionNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.ReferenceTypeId,
|
|
m_context.ModelDesign.GetNodeIdConstant(
|
|
node.ReferenceType,
|
|
"<ReferenceType>",
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.ModellingRuleId,
|
|
GetModellingRuleReplacement(
|
|
GetEffectiveModellingRule(nodeToGenerate, node)));
|
|
context.Template.AddReplacement(
|
|
Tokens.EventNotifier,
|
|
node.SupportsEvents || HasForwardEventReferences(references)
|
|
? "global::Opc.Ua.EventNotifiers.SubscribeToEvents"
|
|
: "global::Opc.Ua.EventNotifiers.None");
|
|
}
|
|
|
|
private void AddMethodStateFactoryReplacements(
|
|
IWriteContext context,
|
|
NodeToGenerate nodeToGenerate,
|
|
MethodDesign node)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.ReferenceTypeId,
|
|
m_context.ModelDesign.GetNodeIdConstant(
|
|
node.ReferenceType,
|
|
"<ReferenceType>",
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassName,
|
|
node.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces));
|
|
context.Template.AddReplacement(
|
|
Tokens.StateClassFactory,
|
|
node.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
asFactory: true));
|
|
context.Template.AddReplacement(
|
|
Tokens.ModellingRuleId,
|
|
GetModellingRuleReplacement(
|
|
GetEffectiveModellingRule(nodeToGenerate, node)));
|
|
bool executable = !node.NonExecutable;
|
|
context.Template.AddReplacement(Tokens.ExecutableValue, executable);
|
|
|
|
context.Template.AddReplacement(
|
|
Tokens.MethodDeclarationId,
|
|
node.MethodDeclarationNode != null
|
|
? CoreUtils.Format(
|
|
"baseState.MethodDeclarationId = {0};",
|
|
(HasResolvableNodeId(node.MethodDeclarationNode) ||
|
|
!node.NumericIdSpecified
|
|
? node.MethodDeclarationNode
|
|
: node).GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable))
|
|
: null);
|
|
AddMethodArgumentsReplacement(
|
|
context,
|
|
nodeToGenerate,
|
|
Tokens.ListOfInputArguments,
|
|
BrowseNames.InputArguments,
|
|
"inputArguments",
|
|
node.InputArguments);
|
|
AddMethodArgumentsReplacement(
|
|
context,
|
|
nodeToGenerate,
|
|
Tokens.ListOfOutputArguments,
|
|
BrowseNames.OutputArguments,
|
|
"outputArguments",
|
|
node.OutputArguments);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the InputArguments or OutputArguments property of a method.
|
|
/// The property is normally produced by its own child node factory,
|
|
/// which carries the well known NodeId of the argument property and
|
|
/// must therefore not be replaced -
|
|
/// <see cref="MethodState.CreateOrReplaceInputArguments"/> keeps the
|
|
/// property that was assigned first. Only the arguments themselves are
|
|
/// re-emitted from the resolved model, and only when the value of the
|
|
/// property cannot be trusted (see
|
|
/// <see cref="RequiresArgumentValueOverride"/>). When no child factory
|
|
/// exists the whole property is created inline.
|
|
/// </summary>
|
|
private void AddMethodArgumentsReplacement(
|
|
IWriteContext context,
|
|
NodeToGenerate nodeToGenerate,
|
|
string token,
|
|
string browseName,
|
|
string propertyName,
|
|
Parameter[] arguments)
|
|
{
|
|
if (arguments == null || arguments.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
bool hasChild = HasArgumentChild(nodeToGenerate, browseName);
|
|
if (hasChild && !RequiresArgumentValueOverride(arguments))
|
|
{
|
|
return;
|
|
}
|
|
context.Template.AddReplacement(
|
|
token,
|
|
hasChild
|
|
? NodeStateTemplates.Assign_MethodArgumentValues
|
|
: NodeStateTemplates.Create_MethodArguments,
|
|
[
|
|
new MethodArgumentsToGenerate(propertyName, browseName, arguments)
|
|
],
|
|
WriteTemplate_MethodArguments);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The value of an argument property imported from a nodeset is emitted
|
|
/// verbatim as the XML of the source nodeset, where the NodeId of the
|
|
/// argument data type is indexed against the namespace table of that
|
|
/// nodeset. Such an index does not survive the import and resolves to
|
|
/// an unrelated namespace at runtime, so the arguments have to be
|
|
/// re-emitted from the resolved model. Arguments that only reference
|
|
/// the OPC UA namespace always decode to the same value and are left
|
|
/// alone.
|
|
/// </summary>
|
|
private static bool RequiresArgumentValueOverride(Parameter[] arguments)
|
|
{
|
|
return Array.Exists(
|
|
arguments,
|
|
argument => argument.DataTypeNode != null &&
|
|
!string.Equals(
|
|
argument.DataTypeNode.SymbolicId.Namespace,
|
|
Namespaces.OpcUa,
|
|
StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The declaration node of a method is only usable as
|
|
/// <see cref="MethodState.MethodDeclarationId"/> when it resolves to a
|
|
/// real NodeId. Method type declarations that are synthesized while
|
|
/// importing a nodeset have no identifier of their own and would emit a
|
|
/// NodeId built from their symbolic name, which does not exist in any
|
|
/// address space. In that case the method node itself is the
|
|
/// declaration.
|
|
/// </summary>
|
|
private static bool HasResolvableNodeId(NodeDesign node)
|
|
{
|
|
return node.NumericIdSpecified ||
|
|
!string.IsNullOrEmpty(node.StringId) ||
|
|
node.HasNonConstantIdentifier() ||
|
|
node.FindNumericIdentifier().HasValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates that the model generates a dedicated child node factory
|
|
/// for the InputArguments or OutputArguments property of a method.
|
|
/// </summary>
|
|
private static bool HasArgumentChild(NodeToGenerate node, string browseName)
|
|
{
|
|
return node.AllChildren.Exists(
|
|
child => child.Design is InstanceDesign instance &&
|
|
string.Equals(
|
|
instance.SymbolicName?.Name,
|
|
browseName,
|
|
StringComparison.Ordinal));
|
|
}
|
|
|
|
private static void AddViewStateFactoryReplacements(
|
|
IWriteContext context,
|
|
ViewDesign node)
|
|
{
|
|
context.Template.AddReplacement(
|
|
Tokens.EventNotifier,
|
|
node.SupportsEvents
|
|
? "global::Opc.Ua.EventNotifiers.SubscribeToEvents"
|
|
: "global::Opc.Ua.EventNotifiers.None");
|
|
context.Template.AddReplacement(Tokens.ContainsNoLoopsValue, node.ContainsNoLoops);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the references include a forward HasEventSource
|
|
/// (i=36) or HasNotifier (i=48) reference. Per OPC UA Part 3, the
|
|
/// EventNotifier attribute must be set on the source node of these
|
|
/// references to indicate that events can be subscribed to.
|
|
/// </summary>
|
|
private static bool HasForwardEventReferences(HashSet<ReferenceToGenerate> references)
|
|
{
|
|
if (references == null)
|
|
{
|
|
return false;
|
|
}
|
|
foreach (ReferenceToGenerate reference in references)
|
|
{
|
|
if (!reference.IsInverse &&
|
|
reference.ReferenceTypeId != null &&
|
|
(reference.ReferenceTypeId.Name == "HasEventSource" ||
|
|
reference.ReferenceTypeId.Name == "HasNotifier"))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void CollectNodesToGenerate()
|
|
{
|
|
foreach (NodeDesign node in m_context.ModelDesign.Nodes)
|
|
{
|
|
if (m_context.ModelDesign.IsExcluded(node))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
NodeToGenerate? entry = null;
|
|
if (node is not InstanceDesign)
|
|
{
|
|
entry = new NodeToGenerate(
|
|
Parent: null,
|
|
Path: string.Empty,
|
|
Hierarchy: node.Hierarchy,
|
|
Design: node,
|
|
IsNotExplicitlyDefined: false,
|
|
RootIsTypeDefinition: true,
|
|
InstanceOf: null,
|
|
IsUnderSingletonInstance: false);
|
|
}
|
|
else
|
|
{
|
|
entry = new NodeToGenerate(
|
|
Parent: null,
|
|
Path: string.Empty,
|
|
Hierarchy: node.Hierarchy,
|
|
Design: node.Hierarchy!.NodeList[0].Instance,
|
|
IsNotExplicitlyDefined: false,
|
|
RootIsTypeDefinition: false,
|
|
InstanceOf: null,
|
|
IsUnderSingletonInstance: true);
|
|
}
|
|
if (!m_nodes.TryAdd(entry.Design!.SymbolicId, entry) &&
|
|
m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Removing duplicate entry for {Node}.",
|
|
entry.Design.SymbolicId.Name);
|
|
}
|
|
GetChildren(entry, m_nodes, false);
|
|
|
|
bool isMethodWithArguments =
|
|
node is MethodDesign method &&
|
|
MethodDesignArgumentResolver.HasMethodArguments(method);
|
|
if ((node is not ObjectTypeDesign and not VariableTypeDesign) &&
|
|
!node.IsMethodTypeDesign() &&
|
|
!isMethodWithArguments)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Add instances to generate
|
|
if (node.Hierarchy == null ||
|
|
!node.Hierarchy.Nodes.TryGetValue(
|
|
string.Empty,
|
|
out HierarchyNode? hierarchyNode))
|
|
{
|
|
continue;
|
|
}
|
|
if (hierarchyNode.Identifier != null)
|
|
{
|
|
if (hierarchyNode.Identifier is uint numericId)
|
|
{
|
|
hierarchyNode.Instance!.NumericId = numericId;
|
|
hierarchyNode.Instance.NumericIdSpecified = true;
|
|
}
|
|
else if (hierarchyNode.Identifier is string stringId)
|
|
{
|
|
hierarchyNode.Instance!.StringId = stringId;
|
|
}
|
|
else if (hierarchyNode.Identifier is Guid or ByteString)
|
|
{
|
|
hierarchyNode.Instance.SetIdentifier(hierarchyNode.Identifier);
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException(CoreUtils.Format(
|
|
"Invalid identifier {0}",
|
|
hierarchyNode.Identifier));
|
|
}
|
|
}
|
|
|
|
var instanceToGenerate = new NodeToGenerate(
|
|
Parent: null,
|
|
Path: string.Empty,
|
|
Hierarchy: node.Hierarchy,
|
|
Design: hierarchyNode.Instance,
|
|
IsNotExplicitlyDefined: false,
|
|
RootIsTypeDefinition: false,
|
|
InstanceOf: entry, // Mark as instance of a type design
|
|
IsUnderSingletonInstance: false);
|
|
entry.Instance = instanceToGenerate;
|
|
if (!m_instances.TryAdd(instanceToGenerate.Design!.SymbolicId, instanceToGenerate) &&
|
|
m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Removing duplicate entry for instance {Node}.",
|
|
instanceToGenerate.Design.SymbolicId.Name);
|
|
}
|
|
GetChildren(instanceToGenerate, m_instances, true);
|
|
}
|
|
|
|
// Collect DataType encoding nodes (Default Binary, Default XML, Default JSON).
|
|
// These are ObjectDesign instances with TypeDefinition=DataTypeEncodingType
|
|
// that are linked to DataTypes via HasEncoding references. They don't have
|
|
// a Hierarchy from the model validation, so we build a minimal one with
|
|
// their references to emit them into the address space.
|
|
CollectEncodingNodes();
|
|
|
|
// Index well-known singleton roots by their TypeDefinition so the
|
|
// child-emission writer can dispatch the type-level factory's
|
|
// forInstance: true branch into the matching singleton-instance
|
|
// child factory (e.g. CreateServerType_GetMonitoredItems can
|
|
// produce InputArguments at the well-known Server_*
|
|
// NodeIds when the parent is the Server singleton).
|
|
BuildSingletonInstanceIndex();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates <see cref="m_singletonsByType"/> with every singleton
|
|
/// instance root or nested well-known instance in
|
|
/// <see cref="m_nodes"/> grouped by its
|
|
/// <see cref="InstanceDesign.TypeDefinitionNode"/>'s SymbolicId.
|
|
/// Top-level entries correspond to declared
|
|
/// <c><opc:Object></c> / <c><opc:Variable></c>
|
|
/// singletons such as <c>Server</c> or
|
|
/// <c>WellKnownRole_Observer</c>; nested entries cover Mandatory
|
|
/// well-known children such as <c>Server_ServerCapabilities</c>
|
|
/// (an instance of <c>ServerCapabilitiesType</c>) so the type-level
|
|
/// factory's child-dispatch can produce the well-known instance
|
|
/// NodeIds at every depth.
|
|
/// </summary>
|
|
private void BuildSingletonInstanceIndex()
|
|
{
|
|
foreach (NodeToGenerate entry in m_nodes.Values)
|
|
{
|
|
if (entry.Design is not InstanceDesign instance)
|
|
{
|
|
continue;
|
|
}
|
|
TypeDesign? typeDef = instance.TypeDefinitionNode;
|
|
if (typeDef == null || typeDef.SymbolicId == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (!m_singletonsByType.TryGetValue(typeDef.SymbolicId,
|
|
out List<NodeToGenerate>? singletons))
|
|
{
|
|
singletons = [];
|
|
m_singletonsByType[typeDef.SymbolicId] = singletons;
|
|
}
|
|
singletons.Add(entry);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given the type-level child <paramref name="typeChild"/> that is
|
|
/// about to be emitted inside a type-level factory body (e.g. the
|
|
/// InputArguments slot inside <c>CreateServerType_GetMonitoredItems</c>),
|
|
/// returns the singleton-instance equivalents — the same relative
|
|
/// path under each known singleton instance of the type root, when
|
|
/// such a child has been collected.
|
|
/// </summary>
|
|
private List<(NodeToGenerate Singleton, NodeToGenerate SingletonChild, string SingletonChildSymbolicId)>
|
|
FindSingletonChildren(NodeToGenerate typeChild)
|
|
{
|
|
var result = new List<
|
|
(
|
|
NodeToGenerate Singleton,
|
|
NodeToGenerate SingletonChild,
|
|
string SingletonChildSymbolicId)>();
|
|
if (typeChild == null || !typeChild.RootIsTypeDefinition)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// Walk up to the type root NodeToGenerate.
|
|
NodeToGenerate typeRoot = typeChild;
|
|
while (typeRoot.Parent != null)
|
|
{
|
|
typeRoot = typeRoot.Parent;
|
|
}
|
|
if (typeRoot.Design is not TypeDesign typeDef ||
|
|
typeDef.SymbolicId == null)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// Find singletons of this type, if any.
|
|
if (!m_singletonsByType.TryGetValue(typeDef.SymbolicId,
|
|
out List<NodeToGenerate>? singletons))
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// The relative path suffix is the part of the child's SymbolicId
|
|
// that follows the type-root SymbolicId. Both type and singleton
|
|
// trees use the same "_<ChildSymbolicName>" path scheme, so we
|
|
// can compute the singleton child SymbolicId by prefix swap.
|
|
string typeRootName = typeDef.SymbolicId.Name;
|
|
string childName = typeChild.Design!.SymbolicId.Name;
|
|
if (childName == null ||
|
|
!childName.StartsWith(typeRootName, StringComparison.Ordinal))
|
|
{
|
|
return result;
|
|
}
|
|
string relativeSuffix = childName[typeRootName.Length..];
|
|
|
|
// Only count singletons whose corresponding child has actually
|
|
// been collected (m_nodes contains the singleton-instance
|
|
// descendants). For example, RoleType has both modifiable
|
|
// (Observer/Operator/...) and immutable (Anonymous/...)
|
|
// singletons; only the modifiable ones have the Mandatory
|
|
// promoted AddIdentity child.
|
|
foreach (NodeToGenerate singleton in singletons)
|
|
{
|
|
if (singleton.Design?.SymbolicId == null)
|
|
{
|
|
continue;
|
|
}
|
|
string singletonChildName = singleton.Design.SymbolicId.Name + relativeSuffix;
|
|
var singletonChildSymbolicId = new XmlQualifiedName(
|
|
singletonChildName,
|
|
typeChild.Design.SymbolicId.Namespace);
|
|
if (m_nodes.TryGetValue(singletonChildSymbolicId,
|
|
out NodeToGenerate? singletonChild))
|
|
{
|
|
result.Add((singleton, singletonChild, singletonChildName));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the <see cref="Tokens.InstanceNodeIdOverride"/>
|
|
/// replacement for the type-level factory currently being emitted.
|
|
/// When the node has well-known singleton instances (e.g. a method
|
|
/// declared under <c>ServerType</c> whose <c>Server</c> singleton
|
|
/// has its own well-known <c>NodeId</c>), the wrapper template
|
|
/// renders a runtime dispatch that rebinds <c>nodeState.NodeId</c> to
|
|
/// the singleton-instance value when <c>forInstance: true</c> is
|
|
/// passed AND <c>parent</c> matches one of the known singleton
|
|
/// instances. The token expands to nothing when no singleton
|
|
/// dispatch is applicable.
|
|
/// </summary>
|
|
private void AddInstanceNodeIdOverrideReplacement(
|
|
IWriteContext context,
|
|
NodeToGenerate node)
|
|
{
|
|
List<(NodeToGenerate Singleton, NodeToGenerate SingletonChild, string SingletonChildSymbolicId)>
|
|
singletonChildren = FindSingletonChildren(node);
|
|
context.Template.AddReplacement(
|
|
Tokens.InstanceNodeIdOverride,
|
|
NodeStateTemplates.InstanceNodeIdOverride,
|
|
singletonChildren.Count > 0
|
|
? [singletonChildren]
|
|
: [],
|
|
WriteTemplate_InstanceNodeIdOverride);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wrapper writer for <see cref="NodeStateTemplates.InstanceNodeIdOverride"/>;
|
|
/// expands the inner <see cref="Tokens.ListOfInstanceNodeIdBranches"/>
|
|
/// token by iterating the collected per-singleton branches.
|
|
/// </summary>
|
|
private bool WriteTemplate_InstanceNodeIdOverride(IWriteContext context)
|
|
{
|
|
if (context.Target is not List<
|
|
(
|
|
NodeToGenerate Singleton,
|
|
NodeToGenerate SingletonChild,
|
|
string SingletonChildSymbolicId)> branches)
|
|
{
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(
|
|
Tokens.ListOfInstanceNodeIdBranches,
|
|
NodeStateTemplates.InstanceNodeIdBranch,
|
|
[.. branches.Cast<object>()],
|
|
WriteTemplate_InstanceNodeIdBranch);
|
|
return context.Template.Render();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-branch writer for <see cref="NodeStateTemplates.InstanceNodeIdBranch"/>;
|
|
/// the first branch emits <c>if</c>, subsequent ones <c>else if</c>.
|
|
/// </summary>
|
|
private bool WriteTemplate_InstanceNodeIdBranch(IWriteContext context)
|
|
{
|
|
if (context.Target is not ValueTuple<NodeToGenerate, NodeToGenerate, string> branch)
|
|
{
|
|
return false;
|
|
}
|
|
context.Template.AddReplacement(
|
|
Tokens.IfOrElseIf,
|
|
context.Index == 0 ? "if" : "else if");
|
|
context.Template.AddReplacement(
|
|
Tokens.ParentNodeIdConstant,
|
|
branch.Item1.Design.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
context.Template.AddReplacement(
|
|
Tokens.NodeIdConstant,
|
|
branch.Item2.Design.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable));
|
|
return context.Template.Render();
|
|
}
|
|
|
|
private void CollectEncodingNodes()
|
|
{
|
|
foreach (NodeDesign node in m_context.ModelDesign.Nodes)
|
|
{
|
|
if (node is not ObjectDesign encoding)
|
|
{
|
|
continue;
|
|
}
|
|
if (encoding.TypeDefinition == null ||
|
|
encoding.TypeDefinition.Name != "DataTypeEncodingType")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (encoding.NotInAddressSpace)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
if (m_context.ModelDesign.IsExcluded(encoding))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip if already collected
|
|
if (m_nodes.ContainsKey(encoding.SymbolicId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Build a synthetic Hierarchy with the encoding's references
|
|
var hierarchy = new Hierarchy();
|
|
if (encoding.References != null)
|
|
{
|
|
foreach (Reference reference in encoding.References)
|
|
{
|
|
if (reference.ReferenceType == null)
|
|
{
|
|
continue;
|
|
}
|
|
hierarchy.References.Add(new HierarchyReference
|
|
{
|
|
SourcePath = string.Empty,
|
|
ReferenceType = reference.ReferenceType,
|
|
IsInverse = reference.IsInverse,
|
|
TargetId = reference.TargetId,
|
|
TargetPath = null
|
|
});
|
|
}
|
|
}
|
|
|
|
encoding.Hierarchy = hierarchy;
|
|
|
|
var entry = new NodeToGenerate(
|
|
Parent: null,
|
|
Path: string.Empty,
|
|
Hierarchy: hierarchy,
|
|
Design: encoding,
|
|
IsNotExplicitlyDefined: false,
|
|
RootIsTypeDefinition: false,
|
|
InstanceOf: null);
|
|
|
|
m_nodes.TryAdd(encoding.SymbolicId, entry);
|
|
}
|
|
}
|
|
|
|
private bool ExcludeNodeStateClassGeneration(NodeToGenerate node)
|
|
{
|
|
// Filter unncessary nodes
|
|
if (!IsInAddressSpace(node))
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Excluded node {Node} as it is marked NotInAddressSpace.",
|
|
node.Design!.SymbolicId.Name);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Only process type designs and method types for definitions
|
|
// Also allow DataTypeEncodingType instances (Default Binary/XML/JSON)
|
|
if (node.Design is not VariableTypeDesign and not ObjectTypeDesign &&
|
|
!node.Design.IsMethodTypeDesign() &&
|
|
!IsDataTypeEncodingInstance(node.Design))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (node.Design!.SymbolicName.Namespace == Namespaces.OpcUa &&
|
|
node.Design.NumericId < 256)
|
|
{
|
|
switch (node.Design.SymbolicName.Name)
|
|
{
|
|
case "DataTypeDictionaryType":
|
|
case "DataTypeDescriptionType":
|
|
case "DataTypeSystemType":
|
|
case "DataTypeEncodingType":
|
|
case "ModellingRuleType":
|
|
break;
|
|
default:
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Skipping built-in node state class generation for {Node}.",
|
|
node.Design.SymbolicId.Name);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool IsInAddressSpace(NodeToGenerate node)
|
|
{
|
|
bool isInAddressSpace = !node.Design!.NotInAddressSpace;
|
|
if (node.Design is InstanceDesign instanceDesign &&
|
|
instanceDesign.TypeDefinition != null &&
|
|
instanceDesign.TypeDefinition.Name == "DataTypeEncodingType")
|
|
{
|
|
isInAddressSpace =
|
|
instanceDesign.Parent == null ||
|
|
!instanceDesign.Parent.NotInAddressSpace;
|
|
}
|
|
return isInAddressSpace;
|
|
}
|
|
|
|
private static bool IsDataTypeEncodingInstance(NodeDesign? node)
|
|
{
|
|
return node is ObjectDesign objectDesign &&
|
|
objectDesign.TypeDefinition != null &&
|
|
objectDesign.TypeDefinition.Name == "DataTypeEncodingType";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the instance is a built in property of a base node state
|
|
/// implementation (variable/method) that should not be generated because
|
|
/// it is already defined on the root nodestate class.
|
|
/// </summary>
|
|
private static bool IsBuiltInProperty(NodeToGenerate node)
|
|
{
|
|
switch (node.Parent?.Design)
|
|
{
|
|
case MethodDesign:
|
|
if (node.Design!.SymbolicName ==
|
|
new XmlQualifiedName("InputArguments", Namespaces.OpcUa))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (node.Design.SymbolicName ==
|
|
new XmlQualifiedName("OutputArguments", Namespaces.OpcUa))
|
|
{
|
|
return true;
|
|
}
|
|
break;
|
|
case VariableDesign:
|
|
case VariableTypeDesign:
|
|
if (node.Design!.SymbolicName ==
|
|
new XmlQualifiedName("EnumStrings", Namespaces.OpcUa))
|
|
{
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
internal static bool HasFixedChildSlot(TypeDesign? typeDefinitionNode, string symbolicName)
|
|
{
|
|
if (typeDefinitionNode == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (typeDefinitionNode.Children?.Items != null)
|
|
{
|
|
foreach (InstanceDesign child in typeDefinitionNode.Children.Items)
|
|
{
|
|
if (child.SymbolicName.Name == symbolicName &&
|
|
child.ModellingRule is ModellingRule.Mandatory or ModellingRule.Optional)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return HasFixedChildSlot(typeDefinitionNode.BaseTypeNode, symbolicName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the effective modelling rule for a <see cref="NodeToGenerate"/>.
|
|
/// When <see cref="GeneratorOptions.UseTypeDefinitionModellingRules"/>
|
|
/// is enabled (opt-in) and the node belongs to an instance hierarchy,
|
|
/// the type definition's modelling rule is used unconditionally.
|
|
/// When the option is off (default), the instance may only
|
|
/// <em>promote</em> the type-definition rule per OPC UA rules:
|
|
/// <list type="bullet">
|
|
/// <item><c>Optional → Mandatory</c></item>
|
|
/// <item><c>OptionalPlaceholder → Mandatory | MandatoryPlaceholder</c></item>
|
|
/// <item><c>MandatoryPlaceholder → Mandatory</c></item>
|
|
/// </list>
|
|
/// Any other instance override (demotion) is ignored and the
|
|
/// type-definition rule is returned instead.
|
|
/// </summary>
|
|
private ModellingRule GetEffectiveModellingRule(
|
|
NodeToGenerate node,
|
|
InstanceDesign instance)
|
|
{
|
|
if (!node.RootIsTypeDefinition &&
|
|
node.TypeDefinitionModellingRule.HasValue)
|
|
{
|
|
ModellingRule typeDefRule = node.TypeDefinitionModellingRule.Value;
|
|
|
|
if (m_context.Options.UseTypeDefinitionModellingRules)
|
|
|
|
{
|
|
return typeDefRule;
|
|
}
|
|
// When not opt-in, only allow valid promotions per
|
|
// the OPC UA modelling rule standard.
|
|
if (!IsValidPromotion(typeDefRule, instance.ModellingRule))
|
|
{
|
|
return typeDefRule;
|
|
}
|
|
}
|
|
|
|
return instance.ModellingRule;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the effective modelling rule for a hierarchy node.
|
|
/// Overload used in <see cref="GetChildren"/> where the hierarchy
|
|
/// node and root-is-type-definition flag are available directly.
|
|
/// </summary>
|
|
private ModellingRule GetEffectiveModellingRule(
|
|
HierarchyNode hierarchyNode,
|
|
InstanceDesign child,
|
|
bool rootIsTypeDefinition)
|
|
{
|
|
if (!rootIsTypeDefinition &&
|
|
hierarchyNode.TypeDefinitionModellingRule.HasValue)
|
|
{
|
|
ModellingRule typeDefRule =
|
|
hierarchyNode.TypeDefinitionModellingRule.Value;
|
|
|
|
if (m_context.Options.UseTypeDefinitionModellingRules)
|
|
|
|
{
|
|
return typeDefRule;
|
|
}
|
|
// When not opt-in, only allow valid promotions per
|
|
// the OPC UA modelling rule standard.
|
|
if (!IsValidPromotion(typeDefRule, child.ModellingRule))
|
|
{
|
|
return typeDefRule;
|
|
}
|
|
}
|
|
|
|
return child.ModellingRule;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the instance modelling rule is a valid
|
|
/// promotion of the type-definition modelling rule per OPC UA:
|
|
/// <list type="bullet">
|
|
/// <item><c>Optional → Mandatory</c></item>
|
|
/// <item><c>OptionalPlaceholder → Mandatory | MandatoryPlaceholder</c></item>
|
|
/// <item><c>MandatoryPlaceholder → Mandatory</c></item>
|
|
/// </list>
|
|
/// Returns <c>true</c> when the instance rule is an allowed
|
|
/// promotion (or identical to the type-definition rule), and
|
|
/// <c>false</c> when it is a demotion that should be rejected.
|
|
/// </summary>
|
|
private static bool IsValidPromotion(
|
|
ModellingRule typeDefRule,
|
|
ModellingRule instanceRule)
|
|
{
|
|
if (instanceRule == typeDefRule)
|
|
{
|
|
return true;
|
|
}
|
|
return typeDefRule switch
|
|
{
|
|
ModellingRule.Optional =>
|
|
instanceRule == ModellingRule.Mandatory,
|
|
ModellingRule.OptionalPlaceholder =>
|
|
instanceRule is ModellingRule.Mandatory
|
|
or ModellingRule.MandatoryPlaceholder,
|
|
ModellingRule.MandatoryPlaceholder =>
|
|
instanceRule == ModellingRule.Mandatory,
|
|
// Mandatory, None, ExposesItsArray, etc. cannot be promoted
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
private void GetChildren(
|
|
NodeToGenerate node,
|
|
Dictionary<XmlQualifiedName, NodeToGenerate> children,
|
|
bool forInstance)
|
|
{
|
|
if (!forInstance)
|
|
{
|
|
if (node.Design!.Children?.Items != null)
|
|
{
|
|
foreach (InstanceDesign child in node.Design.Children.Items)
|
|
{
|
|
if (m_context.ModelDesign.IsExcluded(child))
|
|
{
|
|
continue;
|
|
}
|
|
string childPath = child.SymbolicName.Name;
|
|
if (!string.IsNullOrEmpty(node.Path))
|
|
{
|
|
childPath = CoreUtils.Format(
|
|
"{0}{1}{2}",
|
|
node.Path,
|
|
NodeDesign.PathChar,
|
|
childPath);
|
|
}
|
|
node.Children[child.SymbolicName.Name] = new NodeToGenerate(
|
|
node,
|
|
childPath,
|
|
node.Hierarchy,
|
|
child,
|
|
IsNotExplicitlyDefined: false,
|
|
RootIsTypeDefinition: node.RootIsTypeDefinition,
|
|
InstanceOf: null);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.Hierarchy == null)
|
|
|
|
{
|
|
return;
|
|
}
|
|
foreach (HierarchyNode current in node.Hierarchy.NodeList)
|
|
{
|
|
string? childPath = current.RelativePath;
|
|
|
|
// only looking for nodes in the current tree.
|
|
if (!childPath!.StartsWith(node.Path!, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// ignore reference to the current base node.
|
|
if (childPath == node.Path)
|
|
{
|
|
continue;
|
|
}
|
|
// relative should always end in the name of the current instance.
|
|
if (!childPath.EndsWith(
|
|
current.Instance!.SymbolicName.Name,
|
|
StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// get the parent path.
|
|
if (childPath.Length <= current.Instance.SymbolicName.Name.Length)
|
|
{
|
|
if (!string.IsNullOrEmpty(node.Path))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int idx = childPath.Length - current.Instance.SymbolicName.Name.Length - 1;
|
|
string parentPath = current.RelativePath![..idx];
|
|
|
|
if (parentPath != node.Path)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(node.Path))
|
|
{
|
|
childPath = childPath[(node.Path.Length + 1)..];
|
|
childPath = CoreUtils.Format(
|
|
"{0}{1}{2}",
|
|
node.Path,
|
|
NodeDesign.PathChar,
|
|
childPath);
|
|
}
|
|
|
|
if (current.Instance is not InstanceDesign child)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
ModellingRule effectiveRule = GetEffectiveModellingRule(
|
|
current, child, node.RootIsTypeDefinition);
|
|
|
|
if (!node.RootIsTypeDefinition &&
|
|
!current.ExplicitlyDefined &&
|
|
effectiveRule != ModellingRule.Mandatory &&
|
|
effectiveRule != ModellingRule.Optional)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!current.ExplicitlyDefined &&
|
|
effectiveRule != ModellingRule.Mandatory &&
|
|
effectiveRule != ModellingRule.None &&
|
|
effectiveRule != ModellingRule.ExposesItsArray &&
|
|
effectiveRule != ModellingRule.OptionalPlaceholder &&
|
|
effectiveRule != ModellingRule.MandatoryPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (node.RootIsTypeDefinition &&
|
|
!current.ExplicitlyDefined &&
|
|
current.Inherited &&
|
|
current.AdHocInstance &&
|
|
effectiveRule != ModellingRule.Mandatory)
|
|
{
|
|
// Required descendants of generic containers such as DI ParameterSet
|
|
// must also initialize derived instances. Keep this shallow-level
|
|
// suppression only for non-mandatory inherited ad-hoc declarations.
|
|
if (!node.Path!.Contains(NodeDesign.PathChar, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
bool add = false;
|
|
if (node.Design is DataTypeDesign or ViewDesign or ReferenceTypeDesign)
|
|
{
|
|
add = true;
|
|
}
|
|
else if (node.RootIsTypeDefinition)
|
|
{
|
|
if (effectiveRule == ModellingRule.Mandatory)
|
|
{
|
|
add = true;
|
|
}
|
|
else if (current.ExplicitlyDefined &&
|
|
effectiveRule == ModellingRule.Optional)
|
|
{
|
|
add = true;
|
|
}
|
|
else if (current.ExplicitlyDefined &&
|
|
(effectiveRule is
|
|
ModellingRule.ExposesItsArray or
|
|
ModellingRule.OptionalPlaceholder or
|
|
ModellingRule.MandatoryPlaceholder))
|
|
{
|
|
add = true;
|
|
}
|
|
else if (current.StaticValue && !current.Inherited)
|
|
{
|
|
add = true;
|
|
}
|
|
else if (effectiveRule == ModellingRule.None &&
|
|
current.ExplicitlyDefined)
|
|
{
|
|
// Include ModellingRule=None children that are explicitly defined
|
|
// on this type (e.g., state machine states/transitions, type-level
|
|
// methods like ConditionRefresh, and type-level properties like
|
|
// Creatable or SupportsFilteredRetain).
|
|
add = true;
|
|
}
|
|
else if (effectiveRule is not ModellingRule.None)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
m_logger.LogDebug(
|
|
"Excluding child node {Node} from generation.",
|
|
current.Instance.SymbolicId.Name);
|
|
}
|
|
}
|
|
}
|
|
else if (effectiveRule == ModellingRule.Mandatory)
|
|
{
|
|
add = true;
|
|
}
|
|
else if (current.ExplicitlyDefined)
|
|
{
|
|
add = true;
|
|
}
|
|
else if (m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.LogInformation(
|
|
"Excluding child node {Node} from generation.",
|
|
current.Instance.SymbolicId.Name);
|
|
}
|
|
|
|
if (add)
|
|
{
|
|
XmlQualifiedName symbolicId = current.Instance.SymbolicId;
|
|
var childNodeToGenerate = new NodeToGenerate(
|
|
node,
|
|
childPath,
|
|
node.Hierarchy,
|
|
child,
|
|
IsNotExplicitlyDefined: !current.ExplicitlyDefined,
|
|
RootIsTypeDefinition: node.RootIsTypeDefinition,
|
|
InstanceOf: null,
|
|
TypeDefinitionModellingRule:
|
|
current.TypeDefinitionModellingRule,
|
|
IsUnderSingletonInstance: node.IsUnderSingletonInstance);
|
|
if (!children.TryAdd(symbolicId, childNodeToGenerate) &&
|
|
m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.LogInformation(
|
|
"Removing duplicate entry for child {Node}.",
|
|
symbolicId.Name);
|
|
}
|
|
node.AllChildren.Add(childNodeToGenerate);
|
|
GetChildren(childNodeToGenerate, children, forInstance);
|
|
}
|
|
}
|
|
}
|
|
|
|
private HashSet<ReferenceToGenerate> GetReferences(NodeToGenerate node)
|
|
{
|
|
if (node.Hierarchy == null)
|
|
{
|
|
return [];
|
|
}
|
|
Hierarchy hierarchy = node.Hierarchy;
|
|
NodeDesign? root = node.Design;
|
|
// A predefined instance of a NodeSet2 input does not create the
|
|
// nodes it omits, so no reference may end in one of them.
|
|
bool omitsUndeclaredNodes = OmitsUndeclaredHierarchyNodes(node);
|
|
HashSet<ReferenceToGenerate> references = [];
|
|
foreach (HierarchyReference reference in hierarchy.References)
|
|
{
|
|
if (reference.SourcePath != node.Path &&
|
|
reference.TargetPath != node.Path)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool isInverse = reference.IsInverse;
|
|
|
|
// The type's GeneratesEvent references describe its instances; an Object or
|
|
// Variable cannot be their source (Part 3 §7.15), so they stay on the type.
|
|
if (!isInverse &&
|
|
reference.DefinedOnType &&
|
|
reference.SourcePath == node.Path &&
|
|
root is ObjectDesign or VariableDesign &&
|
|
IsGeneratesEventReference(reference.ReferenceType))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (reference.TargetId != null)
|
|
{
|
|
if (!m_context.ModelDesign.TryFindNode(
|
|
reference.TargetId,
|
|
root!.SymbolicId.Name,
|
|
reference.ReferenceType!.Name,
|
|
out NodeDesign? targetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
references.Add(new ReferenceToGenerate(
|
|
targetNode,
|
|
reference.ReferenceType,
|
|
isInverse));
|
|
continue;
|
|
}
|
|
|
|
if (reference.TargetPath != null &&
|
|
reference.TargetPath.Length == 0 &&
|
|
node.Parent?.Design != null)
|
|
{
|
|
// An empty target path names the root of the hierarchy
|
|
// (the type or the top-level instance), which is only the
|
|
// immediate parent for a direct child.
|
|
NodeToGenerate hierarchyRoot = node.Parent;
|
|
while (hierarchyRoot.Parent?.Design != null)
|
|
{
|
|
hierarchyRoot = hierarchyRoot.Parent;
|
|
}
|
|
references.Add(new ReferenceToGenerate(
|
|
hierarchyRoot.Design!,
|
|
reference.ReferenceType!,
|
|
isInverse));
|
|
continue;
|
|
}
|
|
if (reference.SourcePath == node.Path)
|
|
{
|
|
if (!hierarchy.Nodes.TryGetValue(
|
|
reference.TargetPath!,
|
|
out HierarchyNode? target))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!target.ExplicitlyDefined && node.RootIsTypeDefinition)
|
|
|
|
{
|
|
continue;
|
|
}
|
|
if (omitsUndeclaredNodes &&
|
|
!IsCreatedInNodeSetAddressSpace(node, reference.TargetPath!))
|
|
{
|
|
continue;
|
|
}
|
|
references.Add(new ReferenceToGenerate(
|
|
target.Instance!,
|
|
reference.ReferenceType!,
|
|
isInverse));
|
|
continue;
|
|
}
|
|
if (!hierarchy.Nodes.TryGetValue(reference.SourcePath!, out HierarchyNode? source))
|
|
{
|
|
continue;
|
|
}
|
|
if (!source.ExplicitlyDefined && node.RootIsTypeDefinition)
|
|
{
|
|
continue;
|
|
}
|
|
if (omitsUndeclaredNodes &&
|
|
!IsCreatedInNodeSetAddressSpace(node, reference.SourcePath!))
|
|
{
|
|
continue;
|
|
}
|
|
references.Add(new ReferenceToGenerate(
|
|
source.Instance!,
|
|
reference.ReferenceType!,
|
|
!isInverse));
|
|
}
|
|
|
|
// Apply the instance filter after resolving both identifier and hierarchy-path targets.
|
|
if (!node.RootIsTypeDefinition)
|
|
{
|
|
references.RemoveWhere(reference => reference.TargetNode is InstanceDesign
|
|
{
|
|
ModellingRule: ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder
|
|
});
|
|
}
|
|
|
|
// Inherited and declared designs can differ structurally while
|
|
// resolving to the same runtime reference.
|
|
HashSet<(string ReferenceTypeId, bool IsInverse, string TargetId)> referenceIds = [];
|
|
references.RemoveWhere(reference => !referenceIds.Add((
|
|
m_context.ModelDesign.GetNodeIdConstant(
|
|
reference.ReferenceTypeId,
|
|
"<ReferenceType>",
|
|
kNamespaceTableContextVariable),
|
|
reference.IsInverse,
|
|
reference.TargetNode.GetNodeIdAsCode(
|
|
m_context.ModelDesign.Namespaces,
|
|
kNamespaceTableContextVariable))));
|
|
return references;
|
|
}
|
|
|
|
/// <summary>
|
|
/// True for GeneratesEvent and its subtypes such as AlwaysGeneratesEvent.
|
|
/// </summary>
|
|
private bool IsGeneratesEventReference(XmlQualifiedName? referenceTypeId)
|
|
{
|
|
var generatesEvent = new XmlQualifiedName("GeneratesEvent", Ua.Types.Namespaces.OpcUa);
|
|
if (referenceTypeId == generatesEvent)
|
|
{
|
|
return true;
|
|
}
|
|
if (referenceTypeId == null ||
|
|
!m_context.ModelDesign.TryFindNode(
|
|
referenceTypeId,
|
|
string.Empty,
|
|
string.Empty,
|
|
out NodeDesign? design))
|
|
{
|
|
return false;
|
|
}
|
|
for (TypeDesign? type = (design as ReferenceTypeDesign)?.BaseTypeNode;
|
|
type != null;
|
|
type = type.BaseTypeNode)
|
|
{
|
|
if (type.SymbolicId == generatesEvent)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private List<InstanceDesign> GetAdditionalChildren(NodeToGenerate node)
|
|
{
|
|
List<InstanceDesign> additionalChildren = [];
|
|
foreach (NodeToGenerate child in node.Children.Values)
|
|
{
|
|
if (child.Design is not InstanceDesign instance)
|
|
{
|
|
continue;
|
|
}
|
|
if (child.IsNotExplicitlyDefined)
|
|
{
|
|
continue;
|
|
}
|
|
if (GetEffectiveModellingRule(child, instance) is
|
|
not ModellingRule.Mandatory and
|
|
not ModellingRule.Optional)
|
|
{
|
|
continue;
|
|
}
|
|
if (instance.IsOverridden() &&
|
|
!IsMethodOverrideWithDifferentClass(instance))
|
|
{
|
|
continue;
|
|
}
|
|
additionalChildren.Add(instance);
|
|
}
|
|
|
|
return additionalChildren;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether a method overrides the supertype's method with
|
|
/// another method state class. The subtype then declares a field and a
|
|
/// property of its own that hide the supertype's, so it also has to
|
|
/// enumerate and remove the child itself: the supertype's field stays
|
|
/// empty.
|
|
/// </summary>
|
|
private bool IsMethodOverrideWithDifferentClass(InstanceDesign instance)
|
|
{
|
|
return instance is MethodDesign &&
|
|
instance.IsOverridden() &&
|
|
!instance.IsOverriddenWithSameClass(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces);
|
|
}
|
|
|
|
private List<InstanceDesign> GetChildrenWithProperties(
|
|
NodeToGenerate node,
|
|
bool excludeBuiltIn = false)
|
|
{
|
|
List<InstanceDesign> childrenWithProperties = [];
|
|
foreach (NodeToGenerate child in node.Children.Values)
|
|
{
|
|
if (child.Design is not InstanceDesign instance)
|
|
{
|
|
continue;
|
|
}
|
|
if (child.IsNotExplicitlyDefined)
|
|
{
|
|
continue;
|
|
}
|
|
if (GetEffectiveModellingRule(child, instance) is
|
|
not ModellingRule.Mandatory and
|
|
not ModellingRule.Optional)
|
|
{
|
|
continue;
|
|
}
|
|
if (instance.IsOverriddenWithSameClass(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces))
|
|
{
|
|
continue;
|
|
}
|
|
if (excludeBuiltIn && IsBuiltInProperty(child))
|
|
{
|
|
continue;
|
|
}
|
|
childrenWithProperties.Add(instance);
|
|
}
|
|
|
|
return childrenWithProperties;
|
|
}
|
|
|
|
private HashSet<RolePermission> GetRolePermissions(NodeDesign node)
|
|
{
|
|
var rolePermissions = new HashSet<RolePermission>();
|
|
// Emit RolePermissions on all nodes (type and instance).
|
|
// Type hierarchy nodes carry permissions as metadata but the server
|
|
// bypasses enforcement via IsPartOfTypeHierarchy at runtime.
|
|
RolePermission[]? nodeRolePermissions =
|
|
(node.RolePermissions?.RolePermission) ?? (node.DefaultRolePermissions?.RolePermission);
|
|
if (nodeRolePermissions != null)
|
|
{
|
|
foreach (RolePermission rp in nodeRolePermissions)
|
|
{
|
|
ObjectDesign? roleNode = m_context.ModelDesign.FindNode<ObjectDesign>(
|
|
rp.Role,
|
|
rp.Role.Name,
|
|
"RoleType");
|
|
|
|
if (roleNode != null)
|
|
|
|
{
|
|
rolePermissions.Add(rp);
|
|
}
|
|
}
|
|
}
|
|
return rolePermissions;
|
|
}
|
|
|
|
private static string? GetDisplayNameValue(NodeDesign node)
|
|
{
|
|
if (node.DisplayName != null)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}, string.Empty, {1}",
|
|
node.DisplayName.Key.AsStringLiteral(),
|
|
(node.DisplayName.Value?.Trim()).AsStringLiteral());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static string? GetDescriptionValue(NodeDesign node)
|
|
{
|
|
if (node.Description != null && !node.Description.IsAutogenerated)
|
|
{
|
|
// The generated templates assign through a nodeState local that
|
|
// is statically typed as NodeState, so models with a child
|
|
// Property named "Description" cannot shadow this assignment.
|
|
return CoreUtils.Format(
|
|
"nodeState.Description = {0};",
|
|
node.Description.GetLocalizedTextAsCode());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static string? GetInverseNameValue(ReferenceTypeDesign node)
|
|
{
|
|
if (!node.Symmetric && node.InverseName != null)
|
|
{
|
|
return CoreUtils.Format(
|
|
"state.InverseName = {0};",
|
|
node.InverseName.GetLocalizedTextAsCode());
|
|
}
|
|
if (node.Symmetric)
|
|
{
|
|
return "state.InverseName = global::Opc.Ua.LocalizedText.Null;";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static string? GetModellingRuleReplacement(ModellingRule modellingRule)
|
|
{
|
|
string? constant = modellingRule switch
|
|
{
|
|
ModellingRule.Mandatory => "global::Opc.Ua.Objects.ModellingRule_Mandatory",
|
|
ModellingRule.Optional => "global::Opc.Ua.Objects.ModellingRule_Optional",
|
|
ModellingRule.MandatoryPlaceholder => "global::Opc.Ua.Objects.ModellingRule_MandatoryPlaceholder",
|
|
ModellingRule.OptionalPlaceholder => "global::Opc.Ua.Objects.ModellingRule_OptionalPlaceholder",
|
|
ModellingRule.ExposesItsArray => "global::Opc.Ua.Objects.ModellingRule_ExposesItsArray",
|
|
_ => null
|
|
};
|
|
|
|
return constant != null
|
|
? CoreUtils.Format("baseState.ModellingRuleId = new global::Opc.Ua.NodeId({0});", constant)
|
|
: null;
|
|
}
|
|
|
|
private string GetNodeIdConstantForDataType(
|
|
VariableTypeDesign type,
|
|
Namespace[] namespaceUris)
|
|
{
|
|
if (!m_context.ModelDesign.UseAllowSubtypes)
|
|
{
|
|
DataTypeDesign? dataType = m_context.ModelDesign.FindNode<DataTypeDesign>(
|
|
type.DataType,
|
|
type.SymbolicId.Name,
|
|
"DataType");
|
|
return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
|
|
}
|
|
return type.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
|
|
}
|
|
|
|
private string GetNodeIdConstantForDataType(
|
|
VariableDesign instance,
|
|
Namespace[] namespaceUris)
|
|
{
|
|
if (!m_context.ModelDesign.UseAllowSubtypes)
|
|
{
|
|
DataTypeDesign? dataType = m_context.ModelDesign.FindNode<DataTypeDesign>(
|
|
instance.DataType,
|
|
instance.SymbolicId.Name,
|
|
"DataType");
|
|
return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
|
|
}
|
|
return instance.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
|
|
}
|
|
|
|
private string GetTemplateParameter(TypeDesign type, out string variantBuilder)
|
|
{
|
|
variantBuilder = "global::Opc.Ua.VariantBuilder";
|
|
if (type is not VariableTypeDesign variableType)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
if (type.BaseTypeNode == null)
|
|
|
|
{
|
|
return "<T>";
|
|
}
|
|
if (GetTemplateParameter(type.BaseTypeNode, out variantBuilder) != "<T>")
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
BasicDataType basicType = variableType.DataTypeNode!.BasicDataType;
|
|
|
|
if (basicType == BasicDataType.BaseDataType) // == Variant, so any
|
|
{
|
|
return "<T>";
|
|
}
|
|
|
|
string scalarName;
|
|
switch (basicType)
|
|
{
|
|
case BasicDataType.UserDefined:
|
|
scalarName = variableType.DataTypeNode.GetClassName(
|
|
m_context.ModelDesign.Namespaces);
|
|
variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName);
|
|
break;
|
|
default:
|
|
scalarName = variableType.DataTypeNode.GetDotNetTypeName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
nullable: NullableAnnotation.NonNullable,
|
|
true);
|
|
variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName);
|
|
break;
|
|
}
|
|
|
|
if (variableType.ValueRank == ValueRank.Scalar)
|
|
{
|
|
return $"<{scalarName}>";
|
|
}
|
|
if (variableType.ValueRank == ValueRank.Array)
|
|
{
|
|
return $"<global::Opc.Ua.ArrayOf<{scalarName}>>";
|
|
}
|
|
if (variableType.ValueRank == ValueRank.OneOrMoreDimensions &&
|
|
variableType.DataTypeNode.SupportsMatrixOf())
|
|
{
|
|
return $"<global::Opc.Ua.MatrixOf<{scalarName}>>";
|
|
}
|
|
variantBuilder = "global::Opc.Ua.VariantBuilder";
|
|
return "<global::Opc.Ua.Variant>";
|
|
}
|
|
|
|
private static void CollectMatchingFields(
|
|
VariableTypeDesign variableType,
|
|
Dictionary<string, Parameter> fields)
|
|
{
|
|
CollectFields(
|
|
variableType.DataTypeNode!,
|
|
variableType.ValueRank,
|
|
string.Empty,
|
|
fields);
|
|
|
|
List<string> availablePaths = [.. fields.Keys];
|
|
|
|
for (int ii = 0; ii < availablePaths.Count; ii++)
|
|
{
|
|
if (!variableType.Hierarchy.Nodes.ContainsKey(availablePaths[ii]))
|
|
{
|
|
fields.Remove(availablePaths[ii]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void CollectFields(
|
|
DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string basePath,
|
|
Dictionary<string, Parameter> fields)
|
|
{
|
|
if (dataType.BasicDataType != BasicDataType.UserDefined ||
|
|
valueRank != ValueRank.Scalar)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (DataTypeDesign? parent = dataType;
|
|
parent != null;
|
|
parent = parent.BaseTypeNode as DataTypeDesign)
|
|
{
|
|
if (parent.Fields != null)
|
|
{
|
|
for (int ii = 0; ii < parent.Fields.Length; ii++)
|
|
{
|
|
Parameter parameter = parent.Fields[ii];
|
|
string fieldPath = parameter.Name;
|
|
|
|
if (!string.IsNullOrEmpty(basePath))
|
|
{
|
|
fieldPath = CoreUtils.Format("{0}_{1}", basePath, parameter.Name);
|
|
}
|
|
|
|
fields[fieldPath] = parameter;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private string? AddXmlInitializerForComplexValue(
|
|
NodeDesign node,
|
|
DataTypeDesign dataType,
|
|
System.Xml.XmlElement element)
|
|
{
|
|
string? xml = element?.OuterXml;
|
|
if (string.IsNullOrEmpty(xml))
|
|
{
|
|
return null;
|
|
}
|
|
string resourceName = CoreUtils.Format("Values.{0}", node.SymbolicId.Name);
|
|
string uniqueName = resourceName;
|
|
for (int i = 0; i < 1000; i++)
|
|
{
|
|
if (m_initializers.TryAdd(uniqueName, new TextResource(uniqueName, xml)))
|
|
{
|
|
// Get code to create the variant from the XML resource reference
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, context)",
|
|
uniqueName);
|
|
}
|
|
uniqueName = resourceName + i;
|
|
}
|
|
throw new InvalidOperationException("Unexpected duplicate resource names");
|
|
}
|
|
|
|
private static void AddVariantAccessor(IWriteContext context, DataTypeDesign dataType)
|
|
{
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined when !dataType.IsEnumeration:
|
|
context.Template.AddReplacement(Tokens.VariantFrom, "FromStructure");
|
|
context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetStructure");
|
|
break;
|
|
default:
|
|
context.Template.AddReplacement(Tokens.VariantFrom, "From");
|
|
context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetValue");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Embed all initializers as source code
|
|
/// </summary>
|
|
private Resource? EmbedInitializers()
|
|
{
|
|
if (m_initializers.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
var initializers = new ResourceGenerator(m_context);
|
|
return initializers.Embed(
|
|
m_context.ModelDesign.TargetNamespace.Prefix,
|
|
"NodeStates.i",
|
|
internalAccess: true,
|
|
[.. m_initializers.Values]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Node state generation template
|
|
/// </summary>
|
|
private record class NodeToGenerate(
|
|
NodeToGenerate? Parent = null,
|
|
string? Path = null,
|
|
Hierarchy? Hierarchy = null,
|
|
NodeDesign? Design = null,
|
|
bool IsNotExplicitlyDefined = false,
|
|
bool RootIsTypeDefinition = false,
|
|
NodeToGenerate? InstanceOf = null,
|
|
ModellingRule? TypeDefinitionModellingRule = null,
|
|
bool IsUnderSingletonInstance = false)
|
|
{
|
|
/// <summary>
|
|
/// Full inherited list of children
|
|
/// </summary>
|
|
public List<NodeToGenerate> AllChildren { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Direclty defined
|
|
/// </summary>
|
|
public Dictionary<string, NodeToGenerate> Children { get; } = [];
|
|
|
|
public NodeToGenerate? Instance { get; set; }
|
|
|
|
/// <inheritdoc/>
|
|
public override string ToString()
|
|
{
|
|
return CoreUtils.Format("{0} (Parent: {1})", Design, Parent);
|
|
}
|
|
}
|
|
|
|
private record class ReferenceToGenerate(
|
|
NodeDesign TargetNode,
|
|
XmlQualifiedName ReferenceTypeId,
|
|
bool IsInverse);
|
|
|
|
/// <summary>
|
|
/// True when the argument's code type is the data type itself (scalar,
|
|
/// ArrayOf or MatrixOf). Every other value rank (ScalarOrArray,
|
|
/// ScalarOrOneDimension, Any) is carried as a plain Variant, see
|
|
/// ModelDesignExtensions.GetMethodArgumentTypeAsCode.
|
|
/// </summary>
|
|
private static bool HasTypedArgumentValueRank(Parameter parameter)
|
|
{
|
|
return parameter.ValueRank is
|
|
ValueRank.Scalar or
|
|
ValueRank.Array or
|
|
ValueRank.OneOrMoreDimensions;
|
|
}
|
|
|
|
private static string GetMethodArgumentIdentifier(
|
|
Parameter parameter,
|
|
bool upperCamelCase = false)
|
|
{
|
|
return parameter.GetGeneratedCodeIdentifier(
|
|
upperCamelCase,
|
|
s_methodArgumentCodeNameScope);
|
|
}
|
|
|
|
private void AssignMethodArgumentCodeNames(
|
|
MethodDesign method,
|
|
Parameter[] inputArguments,
|
|
Parameter[] outputArguments)
|
|
{
|
|
string declaredClassName = method.GetNodeStateClassName(
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
[],
|
|
applyStandardFallback: false);
|
|
ModelDesignExtensions.AssignMethodArgumentCodeNames(
|
|
inputArguments,
|
|
outputArguments,
|
|
s_methodArgumentCodeNameScope,
|
|
declaredClassName + "Result");
|
|
}
|
|
|
|
private sealed record MethodArgumentsToGenerate(
|
|
string PropertyName,
|
|
string BrowseName,
|
|
Parameter[] Arguments);
|
|
|
|
private sealed record ImportFactoryRegistration(
|
|
string NodeClass,
|
|
string Discriminator,
|
|
string DiscriminatorId,
|
|
string CreateExpression);
|
|
|
|
private const string kNamespaceTableContextVariable = "context.NamespaceUris";
|
|
|
|
private static readonly XmlQualifiedName s_inputArgumentsBrowseName = new(
|
|
"InputArguments",
|
|
Namespaces.OpcUa);
|
|
|
|
private static readonly XmlQualifiedName s_outputArgumentsBrowseName = new(
|
|
"OutputArguments",
|
|
Namespaces.OpcUa);
|
|
|
|
private static readonly MethodArgumentCodeNameScope s_methodArgumentCodeNameScope = new(
|
|
"_",
|
|
"await",
|
|
"ct",
|
|
"cancellationToken",
|
|
"context",
|
|
"_context",
|
|
"inputArguments",
|
|
"_inputArguments",
|
|
"method",
|
|
"_method",
|
|
"objectId",
|
|
"_objectId",
|
|
"onCall",
|
|
"onCallAsync",
|
|
"outputArguments",
|
|
"_outputArguments",
|
|
"_result",
|
|
"results",
|
|
"serviceResult",
|
|
"ServiceResult");
|
|
|
|
private static readonly string[] s_builtInPropertyNames =
|
|
[
|
|
"Description",
|
|
"Save",
|
|
"Handle",
|
|
"Specification",
|
|
"Update",
|
|
"Delete",
|
|
// Children whose generated accessor property shadows the
|
|
// identically-named global::Opc.Ua.NodeState.Extensions property,
|
|
// which carries the NodeSet2 <Extensions> elements rather than an
|
|
// address-space member, and therefore must be declared "public new".
|
|
"Extensions",
|
|
// Method children whose generated accessor property shadows the
|
|
// identically-named global::Opc.Ua.NodeState.Validate(ISystemContext)
|
|
// instance method and therefore must be declared "public new".
|
|
"Validate"
|
|
];
|
|
|
|
/// <summary>
|
|
/// True when a child property named <paramref name="name"/> on the
|
|
/// generated class of <paramref name="parent"/> hides an inherited
|
|
/// member of the runtime base class and must be declared
|
|
/// <c>public new</c>.
|
|
/// </summary>
|
|
internal static bool RequiresNewModifier(NodeDesign parent, string name)
|
|
{
|
|
return s_builtInPropertyNames.Contains(name) ||
|
|
HidesRuntimeBaseMember(parent, name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// True when a child property named <paramref name="name"/> on the
|
|
/// generated class of <paramref name="parent"/> hides a member it
|
|
/// inherits from the runtime base class (for example a child Property
|
|
/// named <c>DataType</c> on a VariableType class hides
|
|
/// <c>BaseVariableState.DataType</c>, one named <c>Categories</c>
|
|
/// hides <c>NodeState.Categories</c>), so it must be declared
|
|
/// <c>public new</c>.
|
|
/// </summary>
|
|
private static bool HidesRuntimeBaseMember(NodeDesign parent, string name)
|
|
{
|
|
return parent switch
|
|
{
|
|
ObjectTypeDesign or ObjectDesign => s_objectStateMemberNames.Value.Contains(name),
|
|
VariableTypeDesign or VariableDesign => s_variableStateMemberNames.Value.Contains(name),
|
|
MethodDesign => s_methodStateMemberNames.Value.Contains(name),
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the names of the members of <paramref name="type"/> and its
|
|
/// base classes that a derived class sees (public and protected), i.e.
|
|
/// the names a generated child property hides. Read from the runtime
|
|
/// types themselves so a member added to them is never missed.
|
|
/// </summary>
|
|
private static HashSet<string> GetInheritedMemberNames(Type type)
|
|
{
|
|
const BindingFlags kFlags = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
|
var names = new HashSet<string>(StringComparer.Ordinal);
|
|
for (Type? current = type; current != null; current = current.BaseType)
|
|
{
|
|
foreach (MemberInfo member in current.GetMembers(kFlags))
|
|
{
|
|
bool visible = member switch
|
|
{
|
|
ConstructorInfo => false,
|
|
MethodInfo method => !method.IsSpecialName && IsVisibleToDerived(method),
|
|
PropertyInfo property => property.GetIndexParameters().Length == 0 &&
|
|
(IsVisibleToDerived(property.GetGetMethod(true)) ||
|
|
IsVisibleToDerived(property.GetSetMethod(true))),
|
|
EventInfo @event => IsVisibleToDerived(@event.GetAddMethod(true)),
|
|
FieldInfo field => !field.IsSpecialName &&
|
|
(field.IsPublic || field.IsFamily || field.IsFamilyOrAssembly),
|
|
Type nested => nested.IsNestedPublic || nested.IsNestedFamily ||
|
|
nested.IsNestedFamORAssem,
|
|
_ => false
|
|
};
|
|
if (visible)
|
|
{
|
|
names.Add(member.Name);
|
|
}
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
|
|
private static bool IsVisibleToDerived(MethodBase? method)
|
|
{
|
|
return method != null &&
|
|
(method.IsPublic || method.IsFamily || method.IsFamilyOrAssembly);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Members inherited by generated Object and ObjectType classes.
|
|
/// </summary>
|
|
private static readonly Lazy<HashSet<string>> s_objectStateMemberNames =
|
|
new(() => GetInheritedMemberNames(typeof(BaseObjectState)));
|
|
|
|
/// <summary>
|
|
/// Members inherited by generated Variable and VariableType classes
|
|
/// ("Value" is handled separately).
|
|
/// </summary>
|
|
private static readonly Lazy<HashSet<string>> s_variableStateMemberNames =
|
|
new(() => GetInheritedMemberNames(typeof(BaseVariableState)));
|
|
|
|
/// <summary>
|
|
/// Members inherited by generated Method classes.
|
|
/// </summary>
|
|
private static readonly Lazy<HashSet<string>> s_methodStateMemberNames =
|
|
new(() => GetInheritedMemberNames(typeof(MethodState)));
|
|
|
|
private static readonly string[] s_builtInMethodNames =
|
|
[
|
|
"Child"
|
|
];
|
|
|
|
private readonly Dictionary<string, Resource> m_initializers = [];
|
|
private readonly Dictionary<XmlQualifiedName, NodeToGenerate> m_nodes = [];
|
|
private readonly Dictionary<XmlQualifiedName, NodeToGenerate> m_instances = [];
|
|
private readonly Dictionary<XmlQualifiedName, List<NodeToGenerate>> m_singletonsByType = [];
|
|
private readonly ILogger m_logger;
|
|
private readonly IServiceMessageContext m_messageContext;
|
|
private readonly IGeneratorContext m_context;
|
|
}
|
|
}
|