mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-28 22:55:49 +08:00
# Description Correct ownership, endpoint validation, policy propagation, subscription delivery, file handling and generated-model behavior across the stack, tools and samples. Add focused regression tests and update the corresponding guides and sample documentation. ### Previously included corrections - Require an active, matching session to generate or access transient ResultTransfer file handles. Cover both sessionless generation and reads against a handle owned by an active session, using live session owners in the Machinery tests. - Keep physical filesystem operations within their configured mount and apply the same confinement checks throughout file operations. - Validate LDS semaphore paths consistently during server registration and registration-store maintenance. ### Individual issues addressed | # | Component | Issue and correction | |---|---|---| | 1 | AI jobs and transfers | Jobs and transfers did not consistently retain their creator's identity and session. Apply ownership checks to descendants, file handles, subscriptions and execution, and recheck deployment permissions before dispatch. | | 2 | Password-management client | Password-bearing calls could use signing-only channels or follow a replacement connection. Capture an encrypted transport generation and keep dispatch bound to it; preserve ordinary non-password edits. | | 3 | GDS and certificate-push clients | Connections omitted hostname verification. Validate the original configured DNS name or IP address before session activation. | | 4 | Certificate push | Private-key uploads could use signing-only calls. Require an encrypted, generation-bound dispatch while preserving certificate-only updates and restoring the caller's identity afterward. | | 5 | MQTT client | The .NET Framework custom-trust callback could discard TLS hostname, certificate-availability and usage errors. Retain those checks while supporting configured issuer trust. | | 6 | DTLS handshake | Certificate trust was not tied to the configured peer address. Match the certificate's DNS or IP identity to the intended endpoint. | | 7 | Replicated address spaces | Serialized nodes lost per-node and descendant permission metadata. Preserve role permissions, user-role permissions and access restrictions, including updates to existing replica nodes. | | 8 | MCP PubSub imports | Imports without groups skipped checks on the effective broker destination. Check the actual endpoint before resolving credentials or activating the connection. | | 9 | Reference-server accounts | A disabled username account could authenticate again with its previous roles. Reject disabled accounts during fresh authentication. | | 10 | Console reference client | The sample disabled hostname checks when connecting to an otherwise trusted peer. Retain the original requested host and validate it before activation. | | 11 | Capture replay and export | Replay and raw export could accept non-capture files or include key material. Require complete PCAP/PCAPNG containers and exclude key sidecars and decryption-secret blocks. | | 12 | PubSub adapter fallback | The fallback client configuration automatically accepted peer certificates. Apply normal trust and hostname validation before activation or reads. | | 13 | Kafka transport | Explicit TLS enablement was checked but did not reach both final Confluent configurations. Use the same effective protocol for validation, producers and consumers. | | 14 | PubSub actions | Configuration rebuilds could widen a connection-local action handler's scope. Preserve its original connection association and reject mismatched dispatch. | | 15 | PubSub readers | Reader-level message-protection settings were accepted but ignored. Apply the effective group and reader settings before accepting and publishing received values. | | 16 | PubSub key groups | Different `SecurityGroupId` values could share one connection-wide key context. Reject mixed groups on a connection and require separate connections for distinct groups. | | 17 | MCP session registrations | Session-name replacement or reuse could retain another registration's credential bindings. Bind access to the registration, caller and authenticated endpoint, and preserve that identity when borrowing sessions. | | 18 | Viewer HTTP host | Requests reached the viewer's OPC UA session without consistent Host, Origin and port checks. Validate the request destination and browser origin before using the session, while retaining supported local MCP clients. | | 19 | Vision file operations | File operations did not consistently apply protected-file checks, and remote handles lacked caller and connection context. Apply the shared checks and use host-side opaque handles bound to caller, target and generation. | | 20 | DTLS record integrity | The integrity-only MAC used a truncated sequence number. Authenticate the full reconstructed sequence number and epoch so records remain distinct across rollover. | | 21 | AMB client sample | Connection setup omitted hostname validation. Preserve the originally requested host and verify the peer identity before activation. | | 22 | Robot sample writes | The R2 `SpeedOverride` callback could target R1 state. Bind each callback to its own robot and apply that robot's write policy. | | 23 | AI result properties | Optional result children inherited remotely writable defaults. Preserve their declaration restrictions and keep server-produced results read-only. | | 24 | WSS peer-trust updates | Existing channels did not consistently respond to peer-trust changes. Revalidate complete chains for forward and reverse WSS channels and close only those that no longer validate. | | 25 | GDS revocation checks | Caller-provided certificates could cause issuer retrieval from embedded URLs. Resolve validation from configured local trust, issuers and CRLs without fetching caller-provided locations. | | 26 | GMS observations | Published correction observations retained writable model defaults. Reject remote writes while preserving publisher-side updates. | | 27 | Production-dataset monitoring | Subscription admission and delivery could use the reporter's context instead of each subscriber's permissions. Evaluate the subscriber at admission and on every delivery, including cancellation and disposal paths. | | 28 | WoT MQTT destinations | Hostless MQTT forms could imply a localhost destination before loopback checks. Reject them before credential lookup or connection attempts and validate the effective endpoint. | | 29 | XRegistry opaque reads | Content-ID reads and notifications did not consistently honor encrypted-read requirements or the currently accepted transport. Recheck the bound endpoint and reject transport downgrades before rebinding, including when protected values are already queued. | | 30 | PubSub capture dissection | Key loading did not consistently check the configured capture-root boundary. Check each actual key path immediately before opening it. | | 31 | PubSub publication queues | Imported WriterGroup queues were not fully checked against publish-topic grants. Check the destination actually used by each group. | | 32 | MCP PubSub mutations | A caller-supplied mode could select subscribe grants for an operation that publishes. Derive the required direction and parent context from the actual mutation. | | 33 | Generated write masks | Model import and generation could widen an explicit `UserWriteMask`. Preserve it through import, copy, merge, inheritance and dependency metadata. | | 34 | Generated method flags | An explicit `UserExecutable=false` could be lost. Preserve its specified state and resulting method behavior through the same model paths. | | 35 | Generated role identifiers | Unresolved role identifiers could cause permission entries to be omitted. Preserve namespace-qualified role IDs and report a diagnostic when a policy cannot be represented. | | 36 | Aggregation-server defaults | The `localhost` default did not reliably constrain listener binding. Use numeric loopback for the default endpoint and command-line fallback. | | 37 | Certificate-validation cache | Cached results could skip current leaf or issuer validity checks. Recheck validity windows on cache hits and keep hostname validation independent of cached chain trust. | | 38 | Software-update folders | Revision names and marker contents containing dot segments could select paths outside the software root. Normalize and check every derived path against that root. | | 39 | USDA arrays | Array elements were not encoded for their string or asset context. Escape them as data and preserve valid composition and round-trip behavior. | | 40 | Adapter hot reload | A changed dataset definition with the same name could keep old fields, subscriptions or cached values active. Compare complete immutable definitions and retire old state before replacement. | | 41 | Adapter index ranges | Subscription publication could ignore `IndexRange` and return the complete value. Preserve the requested projection and its status and timestamps; reject unsupported range providers. | | 42 | PubSub metadata and targets | Metadata from one connection could change another connection's target mapping. Scope metadata to its connection and provenance, and resolve targets using actual typed field IDs rather than field names or unintended positional fallback. | | 43 | Scales product locks | Product locks were not applied across every remote mutation path. Apply them consistently to value writes, filling calculations and related recipe/controller operations while retaining permitted updates. | | 44 | Event permission caching | Cached event decisions could outlive identity, role or node-policy changes. Revalidate the current subscriber, event type and source permissions instead of reusing a stale decision. | | 45 | WoT Modbus destinations | Hostless Modbus forms could imply localhost before loopback checks. Reject them before connecting and validate the effective endpoint. | | 46 | MCP local PKI tools | Peer-trust mutations did not consistently require the caller's `CertificateAdministration` grant. Check the capability before trust and untrust operations. | | 47 | Robot-control HTTP bridge | The bridge lacked consistent Host, Origin and port validation and caller-specific session grants. Apply those checks before borrowing the OPC UA session and retain supported local clients. | | 48 | Subscription transfers | Transferred subscriptions could retain cached notifications after the destination's permissions changed. Require authorization continuity between source and destination identities and roles, invalidate stale retransmission data when continuity fails, and preserve retransmission behavior for compatible transfers. | ### Compatibility notes - Replication uses the versioned NSRP v1 policy envelope. Upgrade peers together and re-encode or reseed legacy topology from the authoritative model; missing permission metadata is not inferred from old records. - Integrity-only DTLS peers must be upgraded together for the full sequence/epoch MAC. AEAD behavior is unchanged. - Distinct PubSub key groups require separate connections. - Custom sessions that cannot capture the actual encrypted transport, and subscription providers that cannot honor a requested range, are rejected rather than silently changing the requested behavior. - Subscription transfers retain retransmission data only when the destination authorization remains compatible with the source snapshot; otherwise the old sequence numbers and cached data are invalidated. ## Related Issues No separate tracking issue is linked. The individual corrections are listed above. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
3982 lines
160 KiB
C#
3982 lines
160 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;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Xml;
|
|
using Opc.Ua.SourceGeneration;
|
|
using Opc.Ua.Types;
|
|
|
|
namespace Opc.Ua.Schema.Model
|
|
{
|
|
/// <summary>
|
|
/// Defines where to add nullable annotations
|
|
/// </summary>
|
|
internal enum NullableAnnotation
|
|
{
|
|
/// <summary>
|
|
/// Types should be non nullable
|
|
/// </summary>
|
|
NonNullable,
|
|
|
|
/// <summary>
|
|
/// Types are nullable except for data types
|
|
/// </summary>
|
|
NullableExceptDataTypes,
|
|
|
|
/// <summary>
|
|
/// All types are nullable
|
|
/// </summary>
|
|
Nullable
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies one generated method-argument symbol scope and the names
|
|
/// reserved by that scope's emitted scaffolding.
|
|
/// </summary>
|
|
internal sealed class MethodArgumentCodeNameScope
|
|
{
|
|
public MethodArgumentCodeNameScope(params string[] reservedNames)
|
|
: this(null, reservedNames)
|
|
{
|
|
}
|
|
|
|
public MethodArgumentCodeNameScope(
|
|
Func<string, bool, IEnumerable<string>>? getCollisionIdentifiers,
|
|
params string[] reservedNames)
|
|
{
|
|
ReservedNames = reservedNames == null ? [] : [.. reservedNames];
|
|
m_getCollisionIdentifiers = getCollisionIdentifiers;
|
|
}
|
|
|
|
public IReadOnlyCollection<string> ReservedNames { get; }
|
|
|
|
public IEnumerable<string> GetCollisionIdentifiers(
|
|
string identifier,
|
|
bool output)
|
|
{
|
|
return m_getCollisionIdentifiers?.Invoke(identifier, output) ?? [identifier];
|
|
}
|
|
|
|
private readonly Func<string, bool, IEnumerable<string>>? m_getCollisionIdentifiers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dotnet code generation support.
|
|
/// </summary>
|
|
internal static class ModelDesignExtensions
|
|
{
|
|
/// <summary>
|
|
/// Get basic data type from data type design.
|
|
/// </summary>
|
|
/// <param name="dataType"></param>
|
|
/// <returns></returns>
|
|
public static BasicDataType DetermineBasicDataType(this DataTypeDesign? dataType)
|
|
{
|
|
if (dataType == null)
|
|
{
|
|
return BasicDataType.BaseDataType;
|
|
}
|
|
if (dataType.IsOptionSet)
|
|
|
|
{
|
|
return BasicDataType.Enumeration;
|
|
}
|
|
// check if it is a built in data type.
|
|
if (dataType.IsBasicDataType(out BasicDataType basicDataType))
|
|
{
|
|
return basicDataType;
|
|
}
|
|
|
|
// recursively search hierarchy if not base data type yet
|
|
BasicDataType basicType = DetermineBasicDataType(
|
|
dataType.BaseTypeNode as DataTypeDesign);
|
|
|
|
// data type is user defined if a sub-type of structure.
|
|
if (basicType == BasicDataType.Structure)
|
|
{
|
|
return BasicDataType.UserDefined;
|
|
}
|
|
return basicType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get type node state class name
|
|
/// </summary>
|
|
public static string GetNodeStateClassName(
|
|
this TypeDesign type,
|
|
Namespace[] namespaces)
|
|
{
|
|
string className = type.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
return CoreUtils.Format("{0}State", className);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
public static string GetNodeStateClassName(
|
|
this InstanceDesign? instance,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
bool asFactory = false,
|
|
bool applyStandardFallback = true)
|
|
{
|
|
if (instance is MethodDesign method)
|
|
{
|
|
string className;
|
|
XmlQualifiedName methodStateIdentity =
|
|
MethodDesignArgumentResolver.ResolveMethodStateIdentity(method);
|
|
if (methodStateIdentity != null)
|
|
{
|
|
className =
|
|
methodStateIdentity.AsFullyQualifiedTypeSymbol(namespaces);
|
|
|
|
if (className.EndsWith("MethodType", StringComparison.Ordinal))
|
|
{
|
|
className = className[..^"MethodType".Length];
|
|
}
|
|
else if (className.EndsWith("Type", StringComparison.Ordinal))
|
|
{
|
|
className = className[..^"Type".Length];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
className = method.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
if (MethodDesignArgumentResolver.HasMethodArguments(method))
|
|
{
|
|
// Only method types have a typed class. A reference to a
|
|
// method that declares its arguments inline uses the base
|
|
// MethodState; declaration names keep the typed name.
|
|
if (applyStandardFallback &&
|
|
!MethodDesignArgumentResolver.HasTypedMethodState(method))
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
|
|
string typedClassName = CoreUtils.Format(
|
|
"{0}{1}MethodState",
|
|
asFactory ? "new " : string.Empty,
|
|
className);
|
|
// Degrade a reference to a standard global::Opc.Ua.*MethodState
|
|
// to the base MethodState when a curated Opc.Ua.Core omits the
|
|
// typed class and the current model pass does not declare it
|
|
// (e.g. the FileDirectoryType methods on a Robotics Programs
|
|
// object). Model-generation-scoped; see
|
|
// StandardMethodStateFallback.
|
|
if (applyStandardFallback &&
|
|
StandardMethodStateFallback.ShouldFallBackToBase(typedClassName))
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
return typedClassName;
|
|
}
|
|
|
|
return CoreUtils.Format(
|
|
"{0}global::Opc.Ua.MethodState",
|
|
asFactory ? "new " : string.Empty);
|
|
}
|
|
|
|
if (instance is not VariableDesign variable)
|
|
|
|
{
|
|
// No state class is emitted for an excluded ObjectType: use
|
|
// the nearest emitted supertype's.
|
|
TypeDesign? typeDefinition = instance!.TypeDefinitionNode;
|
|
while (typeDefinition is ObjectTypeDesign { IsExcludedFromGeneration: true } &&
|
|
typeDefinition.BaseTypeNode != null)
|
|
{
|
|
typeDefinition = typeDefinition.BaseTypeNode;
|
|
}
|
|
return GetNodeStateNameSimple(typeDefinition!);
|
|
}
|
|
var variableType = instance.TypeDefinitionNode as VariableTypeDesign;
|
|
|
|
// A VariableType materialised from a referenced assembly's
|
|
// dependency payload used to arrive without its DataType
|
|
// restriction, leaving DataTypeNode null and crashing here with a
|
|
// bare NullReferenceException that named neither the variable nor
|
|
// the type. The payload now carries the restriction; this check
|
|
// keeps any remaining gap diagnosable instead of unreadable.
|
|
if (variableType?.DataTypeNode == null)
|
|
{
|
|
throw new InvalidDataException(
|
|
$"The data type of variable type " +
|
|
$"'{instance.TypeDefinition}' could not be resolved, so the " +
|
|
$"node state class of '{instance.SymbolicId?.Name ?? instance.BrowseName}' " +
|
|
"cannot be determined. The type is most likely supplied by a " +
|
|
"referenced assembly whose dependency payload predates the " +
|
|
"VariableType data type entry - rebuild that assembly.");
|
|
}
|
|
|
|
// check if the variable type restricted the datatype to eliminate the
|
|
// need for a template parameter.
|
|
if (variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// check if the variable instance did not restrict the datatype.
|
|
if (!variable.DataTypeNode!.IsTemplateParameterRequired(variable.ValueRank))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// instance restricted the datatype but the type did not.
|
|
string scalarName = GetDotNetTypeName(
|
|
variable.DataTypeNode,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable,
|
|
true);
|
|
|
|
if (variable.ValueRank == ValueRank.Array)
|
|
{
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.ArrayOf<{1}>>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.ArrayOf<{1}>>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
}
|
|
|
|
if (variable.ValueRank == ValueRank.OneOrMoreDimensions &&
|
|
variable.DataTypeNode!.SupportsMatrixOf() &&
|
|
variable.DataTypeNode!.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer)
|
|
{
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.MatrixOf<{1}>>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
return CoreUtils.Format(
|
|
"{0}State<global::Opc.Ua.MatrixOf<{1}>>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
}
|
|
|
|
if (IsIndeterminateType(variable))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
// add hack for TwoStateDiscreteType which always has to be bool.
|
|
if (variableType.SymbolicName ==
|
|
new XmlQualifiedName("TwoStateDiscreteType", Namespaces.OpcUa))
|
|
{
|
|
return GetNodeStateNameSimple(variableType);
|
|
}
|
|
|
|
if (asFactory)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}State<{1}>.With<{2}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName,
|
|
GetVariantBuilder(variable.DataTypeNode, scalarName));
|
|
}
|
|
|
|
return CoreUtils.Format(
|
|
"{0}State<{1}>",
|
|
GetClassName(variableType, namespaces),
|
|
scalarName);
|
|
|
|
string GetNodeStateNameSimple(TypeDesign variableType)
|
|
{
|
|
return CoreUtils.Format(
|
|
"{0}{1}State",
|
|
asFactory ? "new " : string.Empty,
|
|
GetClassName(variableType, namespaces));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the variant builder to use to access or mutate a variant of type T
|
|
/// </summary>
|
|
public static string GetVariantBuilder(
|
|
this DataTypeDesign? datatype,
|
|
string typeName)
|
|
{
|
|
switch (datatype!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined:
|
|
if (datatype.IsEnumeration)
|
|
{
|
|
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
|
|
}
|
|
return CoreUtils.Format("global::Opc.Ua.StructureBuilder<{0}>", typeName);
|
|
case BasicDataType.Enumeration:
|
|
if (datatype.SymbolicId ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
return "global::Opc.Ua.VariantBuilder";
|
|
}
|
|
if (datatype.IsOptionSet)
|
|
{
|
|
return GetVariantBuilder((DataTypeDesign)datatype.BaseTypeNode!, typeName);
|
|
}
|
|
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
|
|
default:
|
|
return "global::Opc.Ua.VariantBuilder";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetBaseClassName(this TypeDesign type, Namespace[] namespaces)
|
|
{
|
|
if (type?.BaseTypeNode?.SymbolicName == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(type));
|
|
}
|
|
if (type is not DataTypeDesign dataType ||
|
|
dataType.BaseTypeNode is not DataTypeDesign dtd)
|
|
{
|
|
return type.BaseTypeNode.GetClassName(namespaces);
|
|
}
|
|
|
|
if (dtd.BasicDataType == BasicDataType.Structure)
|
|
|
|
{
|
|
return "global::Opc.Ua.IEncodeable";
|
|
}
|
|
return dtd.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fully qualify a symbol
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string AsFullyQualifiedTypeSymbol(
|
|
this XmlQualifiedName symbol,
|
|
Namespace[] namespaces,
|
|
string? alternativeName = null)
|
|
{
|
|
string? name = alternativeName ?? symbol?.Name;
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
throw new ArgumentException("Symbol name is empty");
|
|
}
|
|
string? ns = namespaces.GetNamespacePrefix((symbol?.Namespace)!);
|
|
if (string.IsNullOrEmpty(ns))
|
|
{
|
|
return name;
|
|
}
|
|
return CoreUtils.Format("global::{0}.{1}", ns, name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the class name to use when creating an instance of the type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsDerivedDataType(this TypeDesign type)
|
|
{
|
|
if (type is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(type));
|
|
}
|
|
if (type is not DataTypeDesign ||
|
|
type.BaseTypeNode is not DataTypeDesign dtd)
|
|
{
|
|
return true;
|
|
}
|
|
return dtd.BasicDataType != BasicDataType.Structure;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a qualifier for the namespace to use in code.
|
|
/// </summary>
|
|
public static string? GetNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaces != null)
|
|
{
|
|
foreach (Namespace ns in namespaces)
|
|
{
|
|
if (ns.Value == namespaceUri)
|
|
{
|
|
return CoreUtils.Format("{0}", ns.Prefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant string for the namespace uri.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string? GetConstantSymbolForNamespace(
|
|
this Namespace[] namespaces,
|
|
string namespaceUri)
|
|
{
|
|
if (namespaces is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(namespaces));
|
|
}
|
|
Namespace? ns = namespaces.GetNamespace(namespaceUri);
|
|
if (ns == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(ns.Name))
|
|
{
|
|
throw new ArgumentException("Found namespace has no name");
|
|
}
|
|
if (string.IsNullOrWhiteSpace(ns.Prefix)) // TODO: should this work?
|
|
{
|
|
return CoreUtils.Format("Namespaces.{0}", ns.Name);
|
|
}
|
|
return CoreUtils.Format("global::{0}.Namespaces.{1}", ns.Prefix, ns.Name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the expression for a namespace URI in generated code: the
|
|
/// model's namespace constant when the model declares the namespace,
|
|
/// otherwise the URI as an escaped string literal.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A namespace the model does not declare can still appear in
|
|
/// generated code: an inherited child keeps the browse namespace of
|
|
/// the model that declared it (DI below a Robotics subtype), and a
|
|
/// reference can target a node of a namespace the NodeSet lists only
|
|
/// in its NamespaceUris.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetNamespaceUriAsCode(
|
|
this Namespace[] namespaces,
|
|
string namespaceUri)
|
|
{
|
|
return namespaces.GetConstantSymbolForNamespace(namespaceUri)
|
|
?? namespaceUri.AsStringLiteral();
|
|
}
|
|
|
|
public static bool IsOverriddenWithSameClass(
|
|
this InstanceDesign instance,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
if (!instance.IsOverridden())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string x = instance.GetMergedInstance().GetNodeStateClassName(targetNamespace, namespaces);
|
|
string y = instance.OveriddenNode.GetNodeStateClassName(targetNamespace, namespaces);
|
|
|
|
// Only the exact same class counts. An override that refines the
|
|
// generic argument (BaseDataVariableState<double> overridden as
|
|
// BaseDataVariableState<int>) is a different class and has to be
|
|
// redeclared, or the subtype keeps exposing the base's value type.
|
|
// (A special case treating every BaseDataVariableState<...> pair as
|
|
// equal used to compare against the unqualified class name, which
|
|
// GetNodeStateClassName never returns - it had no effect and this
|
|
// is the behaviour the generated code has always had.)
|
|
return x == y;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether an override changes only the type argument of a
|
|
/// generic node state class, e.g. a <c>PropertyState<ArrayOf<A>></c>
|
|
/// overridden as <c>PropertyState<ArrayOf<B>></c> because the
|
|
/// subtype refines the DataType of the child to a subtype of
|
|
/// <c>A</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Two constructions of one generic class are unrelated types, so the
|
|
/// child of the supertype can neither be cast to nor assigned from the
|
|
/// class of the subtype.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentNullException">
|
|
/// <paramref name="instance"/> is <c>null</c>.
|
|
/// </exception>
|
|
public static bool IsOverriddenWithRefinedGenericClass(
|
|
this InstanceDesign instance,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
if (!instance.IsOverridden())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string x = instance.GetMergedInstance().GetNodeStateClassName(targetNamespace, namespaces);
|
|
string y = instance.OveriddenNode.GetNodeStateClassName(targetNamespace, namespaces);
|
|
if (x == y)
|
|
{
|
|
return false;
|
|
}
|
|
int genericStart = x.IndexOf('<', StringComparison.Ordinal);
|
|
return genericStart > 0 &&
|
|
genericStart == y.IndexOf('<', StringComparison.Ordinal) &&
|
|
string.CompareOrdinal(x, 0, y, 0, genericStart) == 0;
|
|
}
|
|
|
|
private static bool IsIndeterminateType(this InstanceDesign instance)
|
|
{
|
|
if (instance is VariableDesign variable)
|
|
{
|
|
if (variable.ValueRank is not ValueRank.Scalar and not ValueRank.Array)
|
|
{
|
|
// Matrix-rank with a concrete data type that supports
|
|
// `MatrixOf<T>` is determinate - the generator emits a
|
|
// typed `State<MatrixOf<T>>` for it. Everything else
|
|
// (ScalarOrArray, ScalarOrOneDimension, Any, or matrix
|
|
// over an abstract numeric base) stays on the Variant
|
|
// fallback.
|
|
return variable.ValueRank != ValueRank.OneOrMoreDimensions ||
|
|
variable.DataTypeNode == null ||
|
|
!variable.DataTypeNode.SupportsMatrixOf() ||
|
|
!(variable.DataTypeNode.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer);
|
|
}
|
|
if (variable.DataType ==
|
|
new XmlQualifiedName(BrowseNames.Enumeration, Namespaces.OpcUa))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static string? EnsureUniqueEnumName(this Parameter target)
|
|
{
|
|
if (target?.Parent is DataTypeDesign dt && dt.HasFields && dt.Fields != null)
|
|
{
|
|
HashSet<string> names = [];
|
|
|
|
foreach (Parameter field in dt.Fields)
|
|
{
|
|
int count = 1;
|
|
string check = field.Name;
|
|
|
|
if (names.Contains(check))
|
|
{
|
|
check = field.Name + "_" + field.Identifier;
|
|
}
|
|
|
|
while (names.Contains(check))
|
|
{
|
|
check = field.Name + "_v" + count++;
|
|
}
|
|
|
|
if (target.Identifier == field.Identifier)
|
|
|
|
{
|
|
return check;
|
|
}
|
|
names.Add(check);
|
|
}
|
|
}
|
|
|
|
return target?.Name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the canonical C# identifier for a method argument.
|
|
/// </summary>
|
|
internal static string GetGeneratedCodeIdentifier(
|
|
this Parameter field,
|
|
bool upperCamelCase = false,
|
|
MethodArgumentCodeNameScope? scope = null)
|
|
{
|
|
string? name = field?.Name;
|
|
scope ??= s_defaultMethodArgumentCodeNameScope;
|
|
if (field != null)
|
|
{
|
|
lock (s_generatedCodeNamesLock)
|
|
{
|
|
if (s_generatedCodeNames.TryGetValue(field, out GeneratedCodeNameState? state) &&
|
|
state.TryGetName(scope, out string generatedName) &&
|
|
!string.IsNullOrEmpty(generatedName))
|
|
{
|
|
name = generatedName;
|
|
}
|
|
}
|
|
}
|
|
return name!.ToCSharpIdentifier(upperCamelCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns unique generated-code names against the requested scope while
|
|
/// preserving the authored OPC UA names.
|
|
/// </summary>
|
|
internal static void AssignMethodArgumentCodeNames(
|
|
this MethodDesign method,
|
|
MethodArgumentCodeNameScope? scope = null,
|
|
params string[] additionalReservedNames)
|
|
{
|
|
AssignMethodArgumentCodeNames(
|
|
(method?.InputArguments)!,
|
|
(method?.OutputArguments)!,
|
|
scope,
|
|
additionalReservedNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns unique generated-code names for an effective method signature.
|
|
/// </summary>
|
|
internal static void AssignMethodArgumentCodeNames(
|
|
Parameter[] inputArguments,
|
|
Parameter[] outputArguments,
|
|
MethodArgumentCodeNameScope? scope = null,
|
|
params string[] additionalReservedNames)
|
|
{
|
|
scope ??= s_defaultMethodArgumentCodeNameScope;
|
|
lock (s_generatedCodeNamesLock)
|
|
{
|
|
ClearGeneratedCodeNames(inputArguments, scope);
|
|
ClearGeneratedCodeNames(outputArguments, scope);
|
|
|
|
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
AddReservedMethodArgumentNames(usedNames, scope.ReservedNames);
|
|
AddReservedMethodArgumentNames(usedNames, additionalReservedNames);
|
|
AssignUniqueGeneratedCodeNames(
|
|
inputArguments,
|
|
usedNames,
|
|
output: false,
|
|
scope);
|
|
AssignUniqueGeneratedCodeNames(
|
|
outputArguments,
|
|
usedNames,
|
|
output: true,
|
|
scope);
|
|
}
|
|
}
|
|
|
|
private static void AddReservedMethodArgumentNames(
|
|
HashSet<string> usedNames,
|
|
IEnumerable<string> reservedNames)
|
|
{
|
|
if (reservedNames == null)
|
|
{
|
|
return;
|
|
}
|
|
foreach (string reservedName in reservedNames)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(reservedName))
|
|
{
|
|
usedNames.Add(GetIdentifierCollisionKey(
|
|
reservedName.ToCSharpIdentifier()));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AssignUniqueGeneratedCodeNames(
|
|
Parameter[] arguments,
|
|
HashSet<string> usedNames,
|
|
bool output,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
if (arguments == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (Parameter argument in arguments)
|
|
{
|
|
if (argument == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string identifier = argument.Name.ToCSharpIdentifier();
|
|
if (TryReserveGeneratedIdentifier(
|
|
identifier,
|
|
output,
|
|
scope,
|
|
usedNames))
|
|
{
|
|
SetGeneratedCodeName(argument, identifier, scope);
|
|
continue;
|
|
}
|
|
|
|
string stem = identifier.TrimStart('@');
|
|
int suffix = output ? 1 : 2;
|
|
while (true)
|
|
{
|
|
string suffixText;
|
|
if (output)
|
|
{
|
|
suffixText = suffix == 1 ?
|
|
"Out" :
|
|
"Out" + suffix.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
else
|
|
{
|
|
suffixText = suffix.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
string candidate = (stem + suffixText).ToCSharpIdentifier();
|
|
suffix++;
|
|
if (TryReserveGeneratedIdentifier(
|
|
candidate,
|
|
output,
|
|
scope,
|
|
usedNames))
|
|
{
|
|
SetGeneratedCodeName(argument, candidate, scope);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryReserveGeneratedIdentifier(
|
|
string identifier,
|
|
bool output,
|
|
MethodArgumentCodeNameScope scope,
|
|
HashSet<string> usedNames)
|
|
{
|
|
string[] collisionKeys =
|
|
[
|
|
.. scope.GetCollisionIdentifiers(identifier, output)
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
.Select(GetIdentifierCollisionKey)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
];
|
|
if (collisionKeys.Any(usedNames.Contains))
|
|
{
|
|
return false;
|
|
}
|
|
foreach (string collisionKey in collisionKeys)
|
|
{
|
|
usedNames.Add(collisionKey);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static void ClearGeneratedCodeNames(
|
|
Parameter[] arguments,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
if (arguments == null)
|
|
{
|
|
return;
|
|
}
|
|
foreach (Parameter argument in arguments)
|
|
{
|
|
if (argument != null &&
|
|
s_generatedCodeNames.TryGetValue(
|
|
argument,
|
|
out GeneratedCodeNameState? state))
|
|
{
|
|
state.ClearName(scope);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetIdentifierCollisionKey(string identifier)
|
|
{
|
|
return identifier.TrimStart('@');
|
|
}
|
|
|
|
private static void SetGeneratedCodeName(
|
|
Parameter argument,
|
|
string name,
|
|
MethodArgumentCodeNameScope scope)
|
|
{
|
|
s_generatedCodeNames.GetOrCreateValue(argument).SetName(scope, name);
|
|
}
|
|
|
|
private sealed class GeneratedCodeNameState
|
|
{
|
|
public bool TryGetName(
|
|
MethodArgumentCodeNameScope scope,
|
|
out string name)
|
|
{
|
|
return m_names.TryGetValue(scope, out name!);
|
|
}
|
|
|
|
public void SetName(
|
|
MethodArgumentCodeNameScope scope,
|
|
string name)
|
|
{
|
|
m_names[scope] = name;
|
|
}
|
|
|
|
public void ClearName(MethodArgumentCodeNameScope scope)
|
|
{
|
|
m_names.Remove(scope);
|
|
}
|
|
|
|
private readonly Dictionary<MethodArgumentCodeNameScope, string> m_names = [];
|
|
}
|
|
|
|
private static readonly MethodArgumentCodeNameScope
|
|
s_defaultMethodArgumentCodeNameScope = new();
|
|
private static readonly System.Threading.Lock s_generatedCodeNamesLock = new();
|
|
private static readonly ConditionalWeakTable<Parameter, GeneratedCodeNameState>
|
|
s_generatedCodeNames =
|
|
#if NET8_0_OR_GREATER
|
|
[];
|
|
#else
|
|
new();
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Returns the field name of a child node.
|
|
/// </summary>
|
|
public static string GetChildFieldName(this Parameter field)
|
|
{
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
// Derived from the property name, which is sanitized and already
|
|
// disambiguated against its siblings. Mapping the authored name
|
|
// directly is lossy in a way the property mapping is not: "Value Id"
|
|
// and "ValueId" both collapse onto "m_valueId" (CS0102, even though
|
|
// their properties differ), and characters that are legal in a
|
|
// BrowseName but not in an identifier survive into the field name -
|
|
// "$Value" became "m_$Value", which does not compile at all.
|
|
return field.GetPropertyName().TrimStart('@').ToSafeSymbolName(true, "m_");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the C# identifier of the property a structure field is
|
|
/// generated as. Structure field names are authored data, so they can be
|
|
/// C# keywords or collide with the members every generated data type
|
|
/// carries; neither would compile when used verbatim. The wire name is
|
|
/// unaffected - it is emitted from <see cref="Parameter.Name"/>.
|
|
/// </summary>
|
|
public static string GetPropertyName(this Parameter field)
|
|
{
|
|
return GetGeneratedName(field, s_propertyNames, BuildPropertyNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks a field's generated name up in the map computed once for its
|
|
/// whole structure. Deciding one field at a time cannot work: three
|
|
/// authored names that sanitize alike ("A-", "A?", "A!" all give "A_")
|
|
/// each see only the others' pre-disambiguation names, so the second and
|
|
/// third both pick the same replacement.
|
|
/// </summary>
|
|
private static string GetGeneratedName(
|
|
Parameter field,
|
|
ConditionalWeakTable<DataTypeDesign, string[]> cache,
|
|
ConditionalWeakTable<DataTypeDesign, string[]>.CreateValueCallback build)
|
|
{
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
if (field.Parent is not DataTypeDesign dataType || dataType.Fields == null)
|
|
{
|
|
return field.Name.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
|
|
int index = IndexOfField(dataType.Fields, field);
|
|
if (index < 0)
|
|
{
|
|
return field.Name.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
return cache.GetValue(dataType, build)[index];
|
|
}
|
|
|
|
private static int IndexOfField(Parameter[] fields, Parameter field)
|
|
{
|
|
for (int ii = 0; ii < fields.Length; ii++)
|
|
{
|
|
if (ReferenceEquals(fields[ii], field))
|
|
{
|
|
return ii;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns every field of a structure its generated property name in one
|
|
/// pass, in declaration order, so each name is unique within the class.
|
|
/// A name is taken by an earlier property, by the backing field of an
|
|
/// earlier property, by a member the templates declare, by the enclosing
|
|
/// type's own name, or by an inherited property.
|
|
/// </summary>
|
|
private static string[] BuildPropertyNames(DataTypeDesign dataType)
|
|
{
|
|
Parameter[] fields = dataType.Fields;
|
|
var names = new string[fields.Length];
|
|
|
|
var taken = new HashSet<string>(StringComparer.Ordinal);
|
|
taken.UnionWith(s_reservedDataTypeMembers);
|
|
taken.UnionWith(s_reservedDataTypeFields);
|
|
if (!string.IsNullOrEmpty(dataType.SymbolicName?.Name))
|
|
{
|
|
// A member may not carry the name of its enclosing type.
|
|
taken.Add(dataType.SymbolicName.Name);
|
|
}
|
|
|
|
// Inherited properties, keyed by generated name. A derived field
|
|
// that carries the *same* authored name as an inherited one is a
|
|
// deliberate redeclaration (the generator emits it as an override),
|
|
// so only a different wire name landing on the same identifier is a
|
|
// collision - that would be CS0108 on the generated class.
|
|
Dictionary<string, string> inherited = CollectInheritedPropertyNames(dataType);
|
|
|
|
for (int ii = 0; ii < fields.Length; ii++)
|
|
{
|
|
Parameter field = fields[ii];
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
names[ii] = string.Empty;
|
|
continue;
|
|
}
|
|
|
|
string candidate = SanitizeFieldName(field.Name);
|
|
string suffixed = candidate + "Field";
|
|
while (IsTaken(taken, inherited, candidate, field.Name))
|
|
{
|
|
candidate = suffixed;
|
|
suffixed += "_";
|
|
}
|
|
|
|
taken.Add(candidate);
|
|
// Reserve the backing field the property is stored in, so two
|
|
// properties cannot share one ("Value" and "value" both map to
|
|
// "m_value", and a field literally named "m_value" would take
|
|
// the name of another field's store).
|
|
taken.Add(ToBackingFieldName(candidate));
|
|
|
|
// Re-apply the keyword escape: the replacement may no longer be
|
|
// one, and the original may still be.
|
|
names[ii] = candidate.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
private static bool IsTaken(
|
|
HashSet<string> taken,
|
|
Dictionary<string, string> inherited,
|
|
string candidate,
|
|
string authoredName)
|
|
{
|
|
return taken.Contains(candidate) ||
|
|
taken.Contains(ToBackingFieldName(candidate)) ||
|
|
(inherited.TryGetValue(candidate, out string? inheritedFrom) &&
|
|
!string.Equals(inheritedFrom, authoredName, StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The generated property names of every field inherited through the
|
|
/// base type chain, mapped to the authored name each came from.
|
|
/// </summary>
|
|
private static Dictionary<string, string> CollectInheritedPropertyNames(
|
|
DataTypeDesign dataType)
|
|
{
|
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
var visited = new HashSet<XmlQualifiedName>();
|
|
|
|
for (var baseType = dataType.BaseTypeNode as DataTypeDesign;
|
|
baseType?.Fields != null;
|
|
baseType = baseType.BaseTypeNode as DataTypeDesign)
|
|
{
|
|
if (baseType.SymbolicId != null && !visited.Add(baseType.SymbolicId))
|
|
{
|
|
// A cyclic base chain is reported elsewhere; do not hang.
|
|
break;
|
|
}
|
|
foreach (Parameter field in baseType.Fields)
|
|
{
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
continue;
|
|
}
|
|
string name = field.GetPropertyName().TrimStart('@');
|
|
if (!result.ContainsKey(name))
|
|
{
|
|
result[name] = field.Name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string SanitizeFieldName(string name)
|
|
{
|
|
return name.ToCSharpIdentifierPreserveCase().TrimStart('@');
|
|
}
|
|
|
|
private static string ToBackingFieldName(string propertyName)
|
|
{
|
|
return propertyName.ToSafeSymbolName(true, "m_");
|
|
}
|
|
|
|
private static readonly ConditionalWeakTable<DataTypeDesign, string[]>
|
|
s_propertyNames =
|
|
#if NET8_0_OR_GREATER
|
|
[];
|
|
#else
|
|
new();
|
|
#endif
|
|
|
|
private static readonly ConditionalWeakTable<DataTypeDesign, string[]>
|
|
s_fieldsEnumMemberNames =
|
|
#if NET8_0_OR_GREATER
|
|
[];
|
|
#else
|
|
new();
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Backing fields the data type templates declare themselves, which a
|
|
/// generated property therefore cannot be named after.
|
|
/// </summary>
|
|
private static readonly HashSet<string> s_reservedDataTypeFields =
|
|
new(StringComparer.Ordinal) { "m_FieldNames", "m_pooledSentinel" };
|
|
|
|
/// <summary>
|
|
/// Returns the C# identifier of the member a structure field is given in
|
|
/// the generated <c>{ClassName}Fields</c> enumeration. Same reasoning as
|
|
/// <see cref="GetPropertyName(Parameter)"/>, but the only name already
|
|
/// taken in that scope is <c>None</c>.
|
|
/// </summary>
|
|
public static string GetFieldsEnumMemberName(this Parameter field)
|
|
{
|
|
return GetGeneratedName(
|
|
field, s_fieldsEnumMemberNames, BuildFieldsEnumMemberNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns every field its member name in the generated
|
|
/// <c>{ClassName}Fields</c> enumeration, in one pass for the same reason
|
|
/// as the property names. The enumeration is its own scope, so the only
|
|
/// name taken up front is <c>None</c>.
|
|
/// </summary>
|
|
private static string[] BuildFieldsEnumMemberNames(DataTypeDesign dataType)
|
|
{
|
|
Parameter[] fields = dataType.Fields;
|
|
var names = new string[fields.Length];
|
|
var taken = new HashSet<string>(StringComparer.Ordinal) { "None" };
|
|
|
|
for (int ii = 0; ii < fields.Length; ii++)
|
|
{
|
|
Parameter field = fields[ii];
|
|
if (string.IsNullOrEmpty(field?.Name))
|
|
{
|
|
names[ii] = string.Empty;
|
|
continue;
|
|
}
|
|
|
|
string candidate = SanitizeFieldName(field.Name);
|
|
string suffixed = candidate + "Field";
|
|
while (taken.Contains(candidate))
|
|
{
|
|
candidate = suffixed;
|
|
suffixed += "_";
|
|
}
|
|
|
|
taken.Add(candidate);
|
|
names[ii] = candidate.ToCSharpIdentifierPreserveCase();
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
private static readonly HashSet<string> s_reservedDataTypeMembers =
|
|
new(StringComparer.Ordinal)
|
|
{
|
|
"SwitchField",
|
|
"EncodingMask",
|
|
"EncodingMaskFieldNames",
|
|
"Reuse",
|
|
"ResetForReuse",
|
|
"TypeId",
|
|
"BinaryEncodingId",
|
|
"XmlEncodingId",
|
|
"JsonEncodingId",
|
|
"Encode",
|
|
"Decode",
|
|
"IsEqual",
|
|
"Equals",
|
|
"GetHashCode",
|
|
"GetType",
|
|
"ToString",
|
|
"Clone",
|
|
"MemberwiseClone",
|
|
"Initialize"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Returns the field name of a child node.
|
|
/// </summary>
|
|
public static string GetChildFieldName(this InstanceDesign instance)
|
|
{
|
|
if (string.IsNullOrEmpty(instance?.SymbolicName?.Name))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
string name = instance.SymbolicName.Name.ToSafeSymbolName(true, "m_");
|
|
|
|
if (instance is MethodDesign)
|
|
{
|
|
return CoreUtils.Format("{0}Method", name);
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixes class names for nodes.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetClassName(this TypeDesign node, Namespace[] namespaces)
|
|
{
|
|
if (node is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(node));
|
|
}
|
|
if (node is DataTypeDesign)
|
|
|
|
{
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
if (node is ObjectTypeDesign objectType &&
|
|
objectType.ClassName == "ObjectSource")
|
|
{
|
|
return "global::Opc.Ua.BaseObject";
|
|
}
|
|
|
|
if (node is VariableTypeDesign variableType &&
|
|
variableType.ClassName is "DataVariable" or "BaseVariable")
|
|
{
|
|
return "global::Opc.Ua.BaseDataVariable";
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(node.ClassName))
|
|
{
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(
|
|
namespaces, node.ClassName);
|
|
}
|
|
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the NodeClass of a Node
|
|
/// </summary>
|
|
public static string GetNodeClassAsString(this NodeDesign node)
|
|
{
|
|
if (node is VariableDesign)
|
|
{
|
|
return "Variable";
|
|
}
|
|
if (node is VariableTypeDesign)
|
|
|
|
{
|
|
return "VariableType";
|
|
}
|
|
if (node is ObjectDesign)
|
|
|
|
{
|
|
return "Object";
|
|
}
|
|
if (node is ObjectTypeDesign)
|
|
|
|
{
|
|
return "ObjectType";
|
|
}
|
|
if (node is ReferenceTypeDesign)
|
|
|
|
{
|
|
return "ReferenceType";
|
|
}
|
|
if (node is DataTypeDesign)
|
|
|
|
{
|
|
return "DataType";
|
|
}
|
|
if (node is MethodDesign)
|
|
|
|
{
|
|
return "Method";
|
|
}
|
|
if (node is ViewDesign)
|
|
|
|
{
|
|
return "View";
|
|
}
|
|
return "Node";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the event notifier flag onto a string.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetEventNotifierAsCode(this ObjectTypeDesign objectType)
|
|
{
|
|
if (objectType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(objectType));
|
|
}
|
|
if (objectType.SupportsEvents)
|
|
{
|
|
return "global::Opc.Ua.EventNotifiers.SubscribeToEvents";
|
|
}
|
|
return "global::Opc.Ua.EventNotifiers.None";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the value rank enumeration onto a string.
|
|
/// </summary>
|
|
public static string GetValueRankAsCode(this ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Array:
|
|
return "global::Opc.Ua.ValueRanks.OneDimension";
|
|
case ValueRank.Scalar:
|
|
return "global::Opc.Ua.ValueRanks.Scalar";
|
|
case ValueRank.Any:
|
|
case ValueRank.ScalarOrArray:
|
|
return "global::Opc.Ua.ValueRanks.Any";
|
|
case ValueRank.ScalarOrOneDimension:
|
|
return "global::Opc.Ua.ValueRanks.ScalarOrOneDimension";
|
|
case ValueRank.OneOrMoreDimensions:
|
|
if (string.IsNullOrWhiteSpace(arrayDimensions))
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
|
|
}
|
|
|
|
// The number of entries in ArrayDimensions is the number of
|
|
// dimensions, which equals the ValueRank for a fixed-size
|
|
// multi-dimensional array (e.g. "3,3" is a two-dimensional
|
|
// array with ValueRank 2).
|
|
string[] dimensions = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries);
|
|
int dims = dimensions.Length;
|
|
if (dims == 0)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
|
|
}
|
|
if (dims == 1)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.OneDimension";
|
|
}
|
|
if (dims == 2)
|
|
{
|
|
return "global::Opc.Ua.ValueRanks.TwoDimensions";
|
|
}
|
|
return CoreUtils.Format("{0}", dims);
|
|
}
|
|
|
|
return "global::Opc.Ua.ValueRanks.Any";
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bit an OptionSet field mask (the design Identifier) selects.
|
|
/// An OptionSet field names exactly one bit (OPC 10000-3 8.40, 8.52);
|
|
/// false for a zero mask (the "no bits set" value) or a mask of
|
|
/// several bits.
|
|
/// </summary>
|
|
public static bool TryGetOptionSetBit(decimal mask, out int bit)
|
|
{
|
|
bit = 0;
|
|
if (mask <= 0 || decimal.Truncate(mask) != mask)
|
|
{
|
|
return false;
|
|
}
|
|
while (decimal.Remainder(mask, 2) == 0)
|
|
{
|
|
mask /= 2;
|
|
bit++;
|
|
}
|
|
return mask == 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bit an OptionSet field names: its explicit
|
|
/// <see cref="Parameter.OptionSetBit"/> (any position, a subtype of
|
|
/// the OptionSet structure has no upper bit, OPC 10000-3 8.40) or
|
|
/// else the bit of its Identifier mask. False for the zero ("no bits
|
|
/// set") value and a mask of several bits.
|
|
/// </summary>
|
|
public static bool TryGetOptionSetBit(this Parameter field, out int bit)
|
|
{
|
|
if (field?.OptionSetBit is int explicitBit && explicitBit >= 0)
|
|
{
|
|
bit = explicitBit;
|
|
return true;
|
|
}
|
|
bit = 0;
|
|
return field != null && TryGetOptionSetBit(field.Identifier, out bit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The decimal mask of an OptionSet bit, or 0 when the bit is beyond
|
|
/// what a decimal represents exactly (bit 96 and above); such a bit
|
|
/// is carried by <see cref="Parameter.OptionSetBit"/>. Computed in
|
|
/// decimal since a signed shift turns bit 63 into a negative mask.
|
|
/// </summary>
|
|
public static decimal GetOptionSetMask(int bit)
|
|
{
|
|
if (bit is < 0 or >= kMaxDecimalMaskBits)
|
|
{
|
|
return 0;
|
|
}
|
|
decimal mask = 1m;
|
|
for (int ii = 0; ii < bit; ii++)
|
|
{
|
|
mask *= 2;
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The hexadecimal BitMask of an OptionSet bit (at least 8 digits),
|
|
/// for any bit position.
|
|
/// </summary>
|
|
public static string GetOptionSetBitMask(int bit)
|
|
{
|
|
string mask = "1248"[bit % 4] + new string('0', bit / 4);
|
|
return mask.Length < 8 ? mask.PadLeft(8, '0') : mask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bit a hexadecimal BitMask of any length selects; false when
|
|
/// the mask is not a single bit.
|
|
/// </summary>
|
|
public static bool TryGetOptionSetBitFromBitMask(string bitMask, out int bit)
|
|
{
|
|
bit = 0;
|
|
string? mask = bitMask?.Trim().TrimStart('0');
|
|
if (string.IsNullOrEmpty(mask))
|
|
{
|
|
return false;
|
|
}
|
|
int zeros = 0;
|
|
for (int ii = mask.Length - 1; ii > 0; ii--)
|
|
{
|
|
if (mask[ii] != '0')
|
|
{
|
|
return false;
|
|
}
|
|
zeros++;
|
|
}
|
|
int lead = mask[0] switch
|
|
{
|
|
'1' => 0,
|
|
'2' => 1,
|
|
'4' => 2,
|
|
'8' => 3,
|
|
_ => -1
|
|
};
|
|
if (lead < 0)
|
|
{
|
|
return false;
|
|
}
|
|
bit = (zeros * 4) + lead;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The bits a decimal OptionSet mask can represent exactly.
|
|
/// </summary>
|
|
private const int kMaxDecimalMaskBits = 96;
|
|
|
|
/// <summary>
|
|
/// The ArrayDimensions a StructureField publishes for the field. A
|
|
/// StructureField ValueRank is -1 or >= 1, never 0 (OPC 10000-3
|
|
/// 8.51), and a multi-dimensional field has at least two dimensions
|
|
/// (OPC 10000-6 5.2.5): a <see cref="ValueRank.OneOrMoreDimensions"/>
|
|
/// field without ArrayDimensions is a two dimensional matrix of
|
|
/// unknown lengths. (The validator already normalizes design fields
|
|
/// that way; this also covers fields it did not see.)
|
|
/// </summary>
|
|
public static string GetStructureFieldArrayDimensions(this Parameter field)
|
|
{
|
|
if (field.ValueRank == ValueRank.OneOrMoreDimensions &&
|
|
string.IsNullOrWhiteSpace(field.ArrayDimensions))
|
|
{
|
|
return "0,0";
|
|
}
|
|
return field.ArrayDimensions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the MinimumSamplingInterval onto a constant.
|
|
/// </summary>
|
|
public static string GetMinimumSamplingIntervalAsCode(this VariableTypeDesign variableType)
|
|
{
|
|
return variableType.MinimumSamplingInterval switch
|
|
{
|
|
-1 => "global::Opc.Ua.MinimumSamplingIntervals.Indeterminate",
|
|
0 => "global::Opc.Ua.MinimumSamplingIntervals.Continuous",
|
|
_ => variableType.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this is a reference type and implicitly nullable
|
|
/// </summary>
|
|
public static bool IsDotNetReferenceType(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
return !IsDotNetValueType(dataType, valueRank);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the data type supports equality comparison in .NET.
|
|
/// </summary>
|
|
public static bool IsDotNetEqualityComparable(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
if (IsDotNetValueType(dataType, valueRank))
|
|
{
|
|
return true;
|
|
}
|
|
if (valueRank == ValueRank.Scalar)
|
|
{
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.String:
|
|
// Use equality for strings
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the data type is a value type in .NET.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsDotNetValueType(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
if (valueRank != ValueRank.Scalar)
|
|
{
|
|
return true;
|
|
// ArrayOf/MatrixOf/Variant;
|
|
}
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
case BasicDataType.SByte:
|
|
case BasicDataType.Byte:
|
|
case BasicDataType.Int16:
|
|
case BasicDataType.UInt16:
|
|
case BasicDataType.Int32:
|
|
case BasicDataType.UInt32:
|
|
case BasicDataType.Int64:
|
|
case BasicDataType.UInt64:
|
|
case BasicDataType.Float:
|
|
case BasicDataType.Double:
|
|
case BasicDataType.DateTime:
|
|
case BasicDataType.Guid:
|
|
case BasicDataType.NodeId:
|
|
case BasicDataType.ExpandedNodeId:
|
|
case BasicDataType.QualifiedName:
|
|
case BasicDataType.LocalizedText:
|
|
case BasicDataType.StatusCode:
|
|
case BasicDataType.Structure: // Extension object
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType: // Variant
|
|
case BasicDataType.Enumeration when !dataType.IsOptionSet:
|
|
case BasicDataType.UserDefined when dataType.IsEnumeration:
|
|
case BasicDataType.ByteString:
|
|
case BasicDataType.XmlElement:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the type needs to be cloned
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool NeedsCloning(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.DataValue:
|
|
case BasicDataType.Structure: // Extension object
|
|
case BasicDataType.BaseDataType: // Variant
|
|
case BasicDataType.UserDefined when !dataType.IsEnumeration:
|
|
case BasicDataType.Enumeration when dataType.IsOptionSet:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the data type can be expressed as a typed
|
|
/// <see cref="MatrixOf{T}"/> in generated code, i.e. whether
|
|
/// <c>Variant.From(MatrixOf<T>)</c> /
|
|
/// <c>Variant.GetXxxMatrix()</c> round-trip APIs exist.
|
|
/// All BasicDataType values except <see cref="BasicDataType.DiagnosticInfo"/>
|
|
/// (which is not a valid structure field per OPC UA Part 5) are
|
|
/// supported.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool SupportsMatrixOf(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(dataType));
|
|
}
|
|
return dataType.BasicDataType != BasicDataType.DiagnosticInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if a field or a nested field of the structure (including the
|
|
/// fields inherited from its base types) is a multi-dimensional array,
|
|
/// i.e. encoded with the inline matrix representation. Such a
|
|
/// structure "is not compatible with the deprecated DataTypeDictionary
|
|
/// mechanism, and shall not be included in a DataTypeDictionary"
|
|
/// (OPC 10000-6 5.2.5): the dictionary has no way to describe values
|
|
/// whose count is the product of the dimensions. A field that allows
|
|
/// subtypes is an ExtensionObject in the dictionary and does not nest.
|
|
/// </summary>
|
|
public static bool HasInlineMatrixField(this DataTypeDesign dataType)
|
|
{
|
|
return HasInlineMatrixField(dataType, []);
|
|
|
|
static bool HasInlineMatrixField(DataTypeDesign dataType, HashSet<DataTypeDesign> visited)
|
|
{
|
|
if (dataType == null || !visited.Add(dataType))
|
|
{
|
|
return false;
|
|
}
|
|
for (DataTypeDesign? type = dataType;
|
|
type != null;
|
|
type = type.BaseTypeNode as DataTypeDesign)
|
|
{
|
|
if (type.Fields == null || type.IsOptionSet || type.IsEnumeration)
|
|
{
|
|
continue;
|
|
}
|
|
foreach (Parameter field in type.Fields)
|
|
{
|
|
if (field == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (field.ValueRank == ValueRank.OneOrMoreDimensions)
|
|
{
|
|
return true;
|
|
}
|
|
if (field.ValueRank is ValueRank.Scalar or ValueRank.Array &&
|
|
!field.AllowSubTypes &&
|
|
field.DataTypeNode is DataTypeDesign fieldType &&
|
|
fieldType.BasicDataType == BasicDataType.UserDefined &&
|
|
!fieldType.IsEnumeration &&
|
|
HasInlineMatrixField(fieldType, visited))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the default value as a C# expression.
|
|
/// </summary>
|
|
/// <param name="dataType">The data type of the value.</param>
|
|
/// <param name="valueRank">The value rank.</param>
|
|
/// <param name="defaultValue">The authored value, decoded here when
|
|
/// <paramref name="decodedValue"/> is null.</param>
|
|
/// <param name="decodedValue">The already decoded value.</param>
|
|
/// <param name="valueAsVariant">Wrap the expression in a Variant.</param>
|
|
/// <param name="targetNamespace">The target namespace.</param>
|
|
/// <param name="namespaces">The design's namespaces.</param>
|
|
/// <param name="context">The context used to decode
|
|
/// <paramref name="defaultValue"/>.</param>
|
|
/// <param name="onUnknownElement">Fallback for values that have no
|
|
/// expression form.</param>
|
|
/// <param name="decodedValueNamespaceUris">The table the namespace
|
|
/// indexes in <paramref name="decodedValue"/> refer to (the NodeSet's);
|
|
/// null for a value authored in the design.</param>
|
|
/// <param name="namespaceTableVariable">A NamespaceTable expression in
|
|
/// scope where the code is emitted (e.g. <c>context.NamespaceUris</c>),
|
|
/// or null when there is none.</param>
|
|
public static string GetValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
System.Xml.XmlElement? defaultValue,
|
|
object? decodedValue,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
IServiceMessageContext context,
|
|
Func<string>? onUnknownElement = null,
|
|
NamespaceTable? decodedValueNamespaceUris = null,
|
|
string? namespaceTableVariable = null)
|
|
{
|
|
TypeInfo decodedValueType = default;
|
|
string defaultString = valueAsVariant ?
|
|
"global::Opc.Ua.Variant.Null" :
|
|
"default";
|
|
|
|
if (decodedValue == null && defaultValue != null)
|
|
{
|
|
// <opc:DefaultValue>
|
|
// <uax:Boolean>true</uax:Boolean>
|
|
// </opc:DefaultValue >
|
|
|
|
// hand-written designs may write empty strings as layout whitespace.
|
|
using var decoder = new XmlDecoder((XmlElement)defaultValue, context)
|
|
{
|
|
TreatWhitespaceOnlyStringsAsEmpty = true
|
|
};
|
|
Variant variant = decoder.ReadVariantValue(null, default);
|
|
decodedValueType = variant.TypeInfo;
|
|
decodedValue = variant.AsBoxedObject(Variant.BoxingBehavior.Legacy);
|
|
// Authored in the design: its indexes are the design's own.
|
|
decodedValueNamespaceUris = null;
|
|
}
|
|
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return dataType.GetArrayValueAsCode(
|
|
defaultValue,
|
|
decodedValue,
|
|
valueAsVariant,
|
|
targetNamespace,
|
|
namespaces,
|
|
onUnknownElement)
|
|
?? defaultString;
|
|
}
|
|
|
|
if (valueRank == ValueRank.Scalar &&
|
|
dataType.BasicDataType != BasicDataType.BaseDataType)
|
|
{
|
|
return dataType.GetScalarValueAsCode(
|
|
defaultValue,
|
|
decodedValue,
|
|
decodedValueType,
|
|
valueAsVariant,
|
|
targetNamespace,
|
|
namespaces,
|
|
onUnknownElement,
|
|
decodedValueNamespaceUris,
|
|
namespaceTableVariable)
|
|
?? defaultString;
|
|
}
|
|
|
|
return onUnknownElement?.Invoke() ?? defaultString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get default for a scalar type
|
|
/// </summary>
|
|
internal static string? GetScalarValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
System.Xml.XmlElement? defaultValue,
|
|
object? decodedValue,
|
|
TypeInfo decodedValueType,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
Func<string>? onUnknownElement = null,
|
|
NamespaceTable? decodedValueNamespaceUris = null,
|
|
string? namespaceTableVariable = null)
|
|
{
|
|
// Note that we return a typed value since we initialize a object/variant
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
if (decodedValue is not bool boolValue)
|
|
{
|
|
boolValue = false;
|
|
}
|
|
return MakeReturnType(boolValue ? "true" : "false");
|
|
case BasicDataType.SByte:
|
|
if (decodedValue is not sbyte sbyteValue)
|
|
{
|
|
sbyteValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(sbyte){0}", sbyteValue));
|
|
case BasicDataType.Byte:
|
|
if (decodedValue is not byte byteValue)
|
|
{
|
|
byteValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(byte){0}", byteValue));
|
|
case BasicDataType.Int16:
|
|
if (decodedValue is not short shortValue)
|
|
{
|
|
shortValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(short){0}", shortValue));
|
|
case BasicDataType.UInt16:
|
|
if (decodedValue is not ushort ushortValue)
|
|
{
|
|
ushortValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(ushort){0}", ushortValue));
|
|
case BasicDataType.Int32:
|
|
if (decodedValue is not int intValue)
|
|
{
|
|
intValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(int){0}", intValue));
|
|
case BasicDataType.UInt32:
|
|
if (decodedValue is not uint uintValue)
|
|
{
|
|
uintValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(uint){0}", uintValue));
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.Int64:
|
|
if (decodedValue is not long longValue)
|
|
{
|
|
longValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(long){0}", longValue));
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.UInt64:
|
|
if (decodedValue is not ulong ulongValue)
|
|
{
|
|
ulongValue = 0;
|
|
}
|
|
return MakeReturnType(CoreUtils.Format("(ulong){0}", ulongValue));
|
|
case BasicDataType.Float:
|
|
if (decodedValue is not float floatValue)
|
|
{
|
|
floatValue = 0;
|
|
}
|
|
// NaN and the infinities have no literal form in C# - they
|
|
// format as "NaN" / "Infinity", which would not compile.
|
|
if (float.IsNaN(floatValue))
|
|
{
|
|
return MakeReturnType("float.NaN");
|
|
}
|
|
if (float.IsPositiveInfinity(floatValue))
|
|
{
|
|
return MakeReturnType("float.PositiveInfinity");
|
|
}
|
|
if (float.IsNegativeInfinity(floatValue))
|
|
{
|
|
return MakeReturnType("float.NegativeInfinity");
|
|
}
|
|
return MakeReturnType("(float)" + FormatFloatLiteral(floatValue));
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Double:
|
|
if (decodedValue is not double doubleValue)
|
|
{
|
|
doubleValue = 0;
|
|
}
|
|
if (double.IsNaN(doubleValue))
|
|
{
|
|
return MakeReturnType("double.NaN");
|
|
}
|
|
if (double.IsPositiveInfinity(doubleValue))
|
|
{
|
|
return MakeReturnType("double.PositiveInfinity");
|
|
}
|
|
if (double.IsNegativeInfinity(doubleValue))
|
|
{
|
|
return MakeReturnType("double.NegativeInfinity");
|
|
}
|
|
return MakeReturnType("(double)" + FormatDoubleLiteral(doubleValue));
|
|
case BasicDataType.String:
|
|
if (decodedValue is not string stringValue)
|
|
{
|
|
return MakeReturnType("default(string)");
|
|
}
|
|
return MakeReturnType(stringValue.AsStringLiteral());
|
|
case BasicDataType.DateTime:
|
|
if (decodedValue is DateTimeUtc dtutc)
|
|
{
|
|
if (dtutc.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
|
|
}
|
|
decodedValue = (DateTime)dtutc;
|
|
}
|
|
if (decodedValue is not DateTime dateTimeValue ||
|
|
dateTimeValue == DateTime.MinValue)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.DateTimeUtc.From(\"{0:yyyy-MM-dd HH:mm:ss}\")",
|
|
dateTimeValue));
|
|
case BasicDataType.Guid:
|
|
if (decodedValue is Uuid uuid)
|
|
{
|
|
decodedValue = uuid.Guid;
|
|
}
|
|
if (decodedValue is not Guid guidValue ||
|
|
guidValue == Guid.Empty)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.Uuid.Empty");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.Uuid.Parse(\"{0}\")",
|
|
guidValue));
|
|
case BasicDataType.ByteString:
|
|
if (decodedValue is ByteString byteStringValue)
|
|
{
|
|
decodedValue = byteStringValue.ToArray();
|
|
}
|
|
if (decodedValue is not byte[] byteArray)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.ByteString.Empty");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ByteString.FromHexString(\"{0}\")",
|
|
CoreUtils.ToHexString(byteArray)));
|
|
case BasicDataType.NodeId:
|
|
if (decodedValue is not NodeId nodeId ||
|
|
nodeId.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.NodeId.Null");
|
|
}
|
|
// The namespace index is one of the table the value was
|
|
// decoded with, not of the server the code runs in: resolve
|
|
// it to its URI and look that up at run time. Without a
|
|
// namespace table in scope (a structure field initializer)
|
|
// or for an index nothing declares, the literal id is all
|
|
// that can be emitted.
|
|
string? nodeIdNamespaceUri = nodeId.NamespaceIndex == 0
|
|
? null
|
|
: ResolveDecodedNamespaceUri(
|
|
nodeId.NamespaceIndex,
|
|
decodedValueNamespaceUris,
|
|
namespaces);
|
|
if (nodeIdNamespaceUri == null || namespaceTableVariable == null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Parse({0})",
|
|
nodeId.ToString().AsStringLiteral()));
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Parse({0}).WithNamespaceIndex({1}.GetIndexOrAppend({2}))",
|
|
nodeId.WithNamespaceIndex(0).ToString().AsStringLiteral(),
|
|
namespaceTableVariable,
|
|
nodeIdNamespaceUri.AsStringLiteral()));
|
|
case BasicDataType.ExpandedNodeId:
|
|
if (decodedValue is not ExpandedNodeId expandedNodeId ||
|
|
expandedNodeId.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.ExpandedNodeId.Null");
|
|
}
|
|
// An ExpandedNodeId can carry the namespace URI itself, so an
|
|
// index-only value is emitted in its absolute "nsu=" form -
|
|
// the index would otherwise be read against the server's
|
|
// namespace table.
|
|
if (string.IsNullOrEmpty(expandedNodeId.NamespaceUri) &&
|
|
expandedNodeId.NamespaceIndex != 0)
|
|
{
|
|
string? expandedNamespaceUri = ResolveDecodedNamespaceUri(
|
|
expandedNodeId.NamespaceIndex,
|
|
decodedValueNamespaceUris,
|
|
namespaces);
|
|
if (expandedNamespaceUri != null)
|
|
{
|
|
expandedNodeId = new ExpandedNodeId(
|
|
expandedNodeId.InnerNodeId,
|
|
expandedNamespaceUri,
|
|
expandedNodeId.ServerIndex);
|
|
}
|
|
}
|
|
// A server index is one of the NodeSet's ServerUris table
|
|
// (OPC 10000-6 F.2, F.14), not of the server the code runs
|
|
// in, and the ExpandedNodeId text has no server URI form:
|
|
// map the URI through the context's ServerUris at run time
|
|
// (the literal index stays the fallback without a table).
|
|
string? serverUri = expandedNodeId.ServerIndex == 0 ||
|
|
decodedValueNamespaceUris is not DecodedValueNamespaceTable decodedTable
|
|
? null
|
|
: decodedTable.ServerUris?.GetString(expandedNodeId.ServerIndex);
|
|
string? serverTableVariable = GetServerTableVariable(namespaceTableVariable);
|
|
if (serverUri != null && serverTableVariable != null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ExpandedNodeId.Parse({0}).WithServerIndex({1}?.GetIndexOrAppend({2}) ?? {3}u)",
|
|
expandedNodeId.WithServerIndex(0).ToString().AsStringLiteral(),
|
|
serverTableVariable,
|
|
serverUri.AsStringLiteral(),
|
|
expandedNodeId.ServerIndex));
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.ExpandedNodeId.Parse({0})",
|
|
expandedNodeId.ToString().AsStringLiteral()));
|
|
case BasicDataType.QualifiedName:
|
|
if (decodedValue is not QualifiedName qualifiedName ||
|
|
qualifiedName.IsNull)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.QualifiedName.Null");
|
|
}
|
|
string? qualifiedNameNamespaceUri = qualifiedName.NamespaceIndex == 0
|
|
? null
|
|
: ResolveDecodedNamespaceUri(
|
|
qualifiedName.NamespaceIndex,
|
|
decodedValueNamespaceUris,
|
|
namespaces);
|
|
if (qualifiedNameNamespaceUri == null || namespaceTableVariable == null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"global::Opc.Ua.QualifiedName.Parse({0})",
|
|
qualifiedName.ToString().AsStringLiteral()));
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new global::Opc.Ua.QualifiedName({0}, {1}.GetIndexOrAppend({2}))",
|
|
qualifiedName.Name.AsStringLiteral(),
|
|
namespaceTableVariable,
|
|
qualifiedNameNamespaceUri.AsStringLiteral()));
|
|
case BasicDataType.LocalizedText:
|
|
if (decodedValue is not Ua.LocalizedText localizedText ||
|
|
localizedText.IsNullOrEmpty)
|
|
{
|
|
return MakeReturnType("global::Opc.Ua.LocalizedText.Null");
|
|
}
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new global::Opc.Ua.LocalizedText({0}, {1})",
|
|
localizedText.Locale.AsStringLiteral(),
|
|
localizedText.Text.AsStringLiteral()));
|
|
case BasicDataType.StatusCode:
|
|
if (decodedValue is not StatusCode statusCode ||
|
|
statusCode.Code == 0)
|
|
{
|
|
return MakeReturnType("default(global::Opc.Ua.StatusCode)");
|
|
}
|
|
// The numeric code, not StatusCode.ToString(): that yields
|
|
// "BadNodeIdUnknown [0x80340000]", which is no C# expression.
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new global::Opc.Ua.StatusCode(0x{0:X8}u)",
|
|
statusCode.Code));
|
|
case BasicDataType.Enumeration:
|
|
if (dataType.BaseTypeNode?.SymbolicId ==
|
|
new XmlQualifiedName("OptionSet", Namespaces.OpcUa))
|
|
{
|
|
// OptionSet subtypes inherit from Opc.Ua.OptionSet (a
|
|
// class implementing IEncodeable), so Variant.From<T>
|
|
// (where T : struct, Enum) does not apply. Route them
|
|
// through Variant.FromStructure<T> (where T : IEncodeable).
|
|
return MakeReturnType(
|
|
CoreUtils.Format("new {0}()",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)),
|
|
"Structure");
|
|
}
|
|
if (!dataType.IsOptionSet && dataType.Fields?.Length > 0)
|
|
{
|
|
if (decodedValue is int enumValue)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format("({0})({1})",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
|
|
enumValue));
|
|
}
|
|
if (decodedValue is null)
|
|
{
|
|
// Enum type declared - use first field as default
|
|
return MakeReturnType(CoreUtils.Format("{0}.{1}",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
|
|
dataType.Fields[0].Name));
|
|
}
|
|
// TODO: change to initialize enum with the value
|
|
return onUnknownElement?.Invoke() ??
|
|
MakeReturnType(CoreUtils.Format("default({0})",
|
|
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)));
|
|
}
|
|
// Default to enum with value 0 which should be "none" for option set
|
|
return onUnknownElement?.Invoke();
|
|
case BasicDataType.DataValue:
|
|
return MakeReturnType("new global::Opc.Ua.DataValue()");
|
|
case BasicDataType.UserDefined:
|
|
if (defaultValue == null)
|
|
{
|
|
return MakeReturnType(CoreUtils.Format(
|
|
"new {0}()",
|
|
GetDotNetTypeName(
|
|
dataType,
|
|
ValueRank.Scalar,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable)),
|
|
"Structure");
|
|
}
|
|
return onUnknownElement?.Invoke();
|
|
default:
|
|
return onUnknownElement?.Invoke();
|
|
}
|
|
|
|
string MakeReturnType(string value, string? prefix = null)
|
|
{
|
|
if (valueAsVariant)
|
|
{
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.From{0}({1})",
|
|
prefix ?? string.Empty,
|
|
value);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The run-time server URI table that sits next to a namespace table
|
|
/// expression: "context.NamespaceUris" pairs with "context.ServerUris".
|
|
/// Null when the expression is not a member access on a context.
|
|
/// </summary>
|
|
private static string? GetServerTableVariable(string? namespaceTableVariable)
|
|
{
|
|
const string suffix = ".NamespaceUris";
|
|
if (namespaceTableVariable == null ||
|
|
!namespaceTableVariable.EndsWith(suffix, StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
return namespaceTableVariable[..(namespaceTableVariable.Length - suffix.Length)] +
|
|
".ServerUris";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a namespace index found inside a decoded default value to
|
|
/// its namespace URI. A value read from a NodeSet carries the table of
|
|
/// that NodeSet; a value authored in a ModelDesign numbers OPC UA 0 and
|
|
/// then the design's namespaces in declaration order. Returns null when
|
|
/// the index is not in the table.
|
|
/// </summary>
|
|
internal static string? ResolveDecodedNamespaceUri(
|
|
ushort namespaceIndex,
|
|
NamespaceTable? decodedValueNamespaceUris,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (namespaceIndex == 0)
|
|
{
|
|
return Namespaces.OpcUa;
|
|
}
|
|
NamespaceTable table = decodedValueNamespaceUris ??
|
|
CreateDesignNamespaceTable(namespaces);
|
|
return table.GetString(namespaceIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The table the namespace indexes of a value authored in a ModelDesign
|
|
/// refer to: OPC UA 0 and then the design's namespaces in declaration
|
|
/// order.
|
|
/// </summary>
|
|
internal static NamespaceTable CreateDesignNamespaceTable(Namespace[] namespaces)
|
|
{
|
|
var table = new NamespaceTable();
|
|
foreach (Namespace ns in namespaces ?? [])
|
|
{
|
|
if (!string.IsNullOrEmpty(ns?.Value))
|
|
{
|
|
table.GetIndexOrAppend(ns.Value);
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a double as the digits of a C# literal that evaluates to
|
|
/// exactly the same value. The default "G" format depends on the
|
|
/// runtime the generator runs in: 15 significant digits on .NET
|
|
/// Framework (Visual Studio), the shortest round-trip form on .NET -
|
|
/// so the generated code differed between the two, lost precision on
|
|
/// .NET Framework and turned double.MaxValue into an out-of-range
|
|
/// literal (CS0594). The shortest of G15..G17 that parses back to the
|
|
/// value is the same on every runtime.
|
|
/// </summary>
|
|
internal static string FormatDoubleLiteral(double value)
|
|
{
|
|
if (value == 0)
|
|
{
|
|
// "(double)-0" would be integer zero cast to +0.
|
|
return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0";
|
|
}
|
|
for (int precision = 15; precision < 17; precision++)
|
|
{
|
|
string text = value.ToString(
|
|
"G" + precision.ToString(CultureInfo.InvariantCulture),
|
|
CultureInfo.InvariantCulture);
|
|
if (double.TryParse(
|
|
text,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out double parsed) &&
|
|
parsed.Equals(value))
|
|
{
|
|
return text;
|
|
}
|
|
}
|
|
return value.ToString("G17", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a float as the digits of a literal that, cast to float,
|
|
/// evaluates to exactly the same value. The generated expression is
|
|
/// <c>(float)digits</c>, i.e. the digits are parsed as a double and then
|
|
/// converted, so the round trip is checked the same way.
|
|
/// See <see cref="FormatDoubleLiteral"/> for why "G" is not used.
|
|
/// </summary>
|
|
internal static string FormatFloatLiteral(float value)
|
|
{
|
|
if (value == 0)
|
|
{
|
|
return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0";
|
|
}
|
|
for (int precision = 6; precision < 9; precision++)
|
|
{
|
|
string text = value.ToString(
|
|
"G" + precision.ToString(CultureInfo.InvariantCulture),
|
|
CultureInfo.InvariantCulture);
|
|
if (double.TryParse(
|
|
text,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out double parsed) &&
|
|
((float)parsed).Equals(value))
|
|
{
|
|
return text;
|
|
}
|
|
}
|
|
return value.ToString("G9", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get default for a array type
|
|
/// </summary>
|
|
internal static string? GetArrayValueAsCode(
|
|
this DataTypeDesign dataType,
|
|
System.Xml.XmlElement? defaultValue,
|
|
object? decodedValue,
|
|
bool valueAsVariant,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
Func<string>? onUnknownElement = null)
|
|
{
|
|
if ((decodedValue is not null &&
|
|
(decodedValue is not IList l || l.Count != 0) &&
|
|
(decodedValue is not Array a || a.Length != 0)) ||
|
|
(decodedValue is null && defaultValue is not null))
|
|
{
|
|
// TODO: change to initialize simple arrays with values
|
|
return onUnknownElement?.Invoke();
|
|
}
|
|
// Emit Array.Empty<T>() for empty arrays
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<bool>()");
|
|
case BasicDataType.SByte:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<sbyte>()");
|
|
case BasicDataType.Byte:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<byte>()");
|
|
case BasicDataType.Int16:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<short>()");
|
|
case BasicDataType.UInt16:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ushort>()");
|
|
case BasicDataType.Int32:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<int>()");
|
|
case BasicDataType.UInt32:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<uint>()");
|
|
case BasicDataType.Int64:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<long>()");
|
|
case BasicDataType.UInt64:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ulong>()");
|
|
case BasicDataType.Float:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<float>()");
|
|
case BasicDataType.Double:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<double>()");
|
|
case BasicDataType.String:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<string>()");
|
|
case BasicDataType.Guid:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Uuid>()");
|
|
case BasicDataType.DateTime:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DateTimeUtc>()");
|
|
case BasicDataType.ByteString:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ByteString>()");
|
|
case BasicDataType.XmlElement:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.XmlElement>()");
|
|
case BasicDataType.NodeId:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.NodeId>()");
|
|
case BasicDataType.ExpandedNodeId:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExpandedNodeId>()");
|
|
case BasicDataType.QualifiedName:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.QualifiedName>()");
|
|
case BasicDataType.LocalizedText:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.LocalizedText>()");
|
|
case BasicDataType.StatusCode:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.StatusCode>()");
|
|
case BasicDataType.DataValue:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DataValue>()");
|
|
case BasicDataType.Structure:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExtensionObject>()");
|
|
case BasicDataType.Enumeration:
|
|
case BasicDataType.UserDefined:
|
|
return MakeReturnType(
|
|
CoreUtils.Format(
|
|
"global::Opc.Ua.ArrayOf.Empty<{0}>()",
|
|
GetDotNetTypeName(
|
|
dataType,
|
|
ValueRank.Scalar,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable: NullableAnnotation.NonNullable)),
|
|
dataType.BasicDataType == BasicDataType.UserDefined ? "Structure" : null);
|
|
case BasicDataType.DiagnosticInfo:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DiagnosticInfo>()");
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Variant>()");
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
string MakeReturnType(string value, string? prefix = null)
|
|
{
|
|
if (valueAsVariant)
|
|
{
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.From{0}({1})",
|
|
prefix ?? string.Empty,
|
|
value);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get code string to load a variant from XML resource stream.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string GetVariantValueFromXmlAsCode(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
string xmlStreamResource,
|
|
string? contextVariable = null)
|
|
{
|
|
contextVariable ??= "context";
|
|
switch (dataType.BasicDataType)
|
|
{
|
|
case BasicDataType.BaseDataType:
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1})",
|
|
xmlStreamResource,
|
|
contextVariable);
|
|
case BasicDataType.UserDefined:
|
|
case BasicDataType.Structure:
|
|
case BasicDataType.Enumeration:
|
|
string dataTypeString = dataType.GetDotNetTypeName(
|
|
targetNamespace,
|
|
namespaces,
|
|
NullableAnnotation.NonNullable,
|
|
false);
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}<{4}>()",
|
|
xmlStreamResource,
|
|
contextVariable,
|
|
dataType.BasicDataType == BasicDataType.Enumeration ? "Enumeration" : "Structure",
|
|
valueRank == ValueRank.Array ? "Array" : string.Empty,
|
|
dataTypeString);
|
|
default:
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}()",
|
|
xmlStreamResource,
|
|
contextVariable,
|
|
dataType.BasicDataType.ToString(),
|
|
valueRank == ValueRank.Array ? "Array" : string.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type for a method argument.
|
|
/// </summary>
|
|
public static string GetMethodArgumentTypeAsCode(
|
|
this DataTypeDesign datatype,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
bool isOptional)
|
|
{
|
|
string typeName = GetDotNetTypeName(
|
|
datatype,
|
|
targetNamespace,
|
|
namespaces,
|
|
// Make scalar optional nullable, but array/matrix elements non nullable
|
|
// TODO: Revisit this and make input non nullable but output parameter T
|
|
// nullable when we get rid of collections
|
|
nullable: isOptional && valueRank == ValueRank.Scalar ?
|
|
NullableAnnotation.Nullable : NullableAnnotation.NonNullable,
|
|
// Use raw arguments
|
|
false);
|
|
|
|
if (typeName is "global::Opc.Ua.IEncodeable" or "global::Opc.Ua.IEncodeable?")
|
|
|
|
{
|
|
typeName = "global::Opc.Ua.ExtensionObject";
|
|
}
|
|
if (typeName is "object" or "object?")
|
|
{
|
|
typeName = "global::Opc.Ua.Variant";
|
|
}
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Scalar:
|
|
return typeName;
|
|
case ValueRank.Array:
|
|
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
|
|
case ValueRank.OneOrMoreDimensions:
|
|
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
|
|
default:
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the type is nullable
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <returns></returns>
|
|
public static bool IsXmlNillable(this BasicDataType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
case BasicDataType.SByte:
|
|
case BasicDataType.Byte:
|
|
case BasicDataType.Int16:
|
|
case BasicDataType.UInt16:
|
|
case BasicDataType.Int32:
|
|
case BasicDataType.UInt32:
|
|
case BasicDataType.Int64:
|
|
case BasicDataType.UInt64:
|
|
case BasicDataType.Float:
|
|
case BasicDataType.Double:
|
|
case BasicDataType.StatusCode:
|
|
case BasicDataType.Enumeration:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns system type for a basic data type.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"></exception>
|
|
public static string GetDotNetTypeName(
|
|
this DataTypeDesign? datatype,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
NullableAnnotation nullable = NullableAnnotation.NonNullable,
|
|
bool useBuiltInWrapperTypes = false)
|
|
{
|
|
switch (datatype!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "bool";
|
|
case BasicDataType.SByte:
|
|
return "sbyte";
|
|
case BasicDataType.Byte:
|
|
return "byte";
|
|
case BasicDataType.Int16:
|
|
return "short";
|
|
case BasicDataType.UInt16:
|
|
return "ushort";
|
|
case BasicDataType.Int32:
|
|
return "int";
|
|
case BasicDataType.UInt32:
|
|
return "uint";
|
|
case BasicDataType.Int64:
|
|
return "long";
|
|
case BasicDataType.UInt64:
|
|
return "ulong";
|
|
case BasicDataType.Float:
|
|
return "float";
|
|
case BasicDataType.Double:
|
|
return "double";
|
|
case BasicDataType.String:
|
|
return nullable == NullableAnnotation.NonNullable ?
|
|
"string" :
|
|
"string?";
|
|
case BasicDataType.DateTime:
|
|
return "global::Opc.Ua.DateTimeUtc";
|
|
case BasicDataType.Guid:
|
|
return "global::Opc.Ua.Uuid";
|
|
case BasicDataType.ByteString:
|
|
return "global::Opc.Ua.ByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "global::Opc.Ua.XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "global::Opc.Ua.NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "global::Opc.Ua.ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "global::Opc.Ua.StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return nullable == NullableAnnotation.NonNullable ?
|
|
"global::Opc.Ua.DiagnosticInfo" :
|
|
"global::Opc.Ua.DiagnosticInfo?";
|
|
case BasicDataType.QualifiedName:
|
|
return "global::Opc.Ua.QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "global::Opc.Ua.LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "global::Opc.Ua.DataValue";
|
|
case BasicDataType.Structure:
|
|
if (!useBuiltInWrapperTypes)
|
|
{
|
|
// return nullable == NullableAnnotation.NonNullable ?
|
|
// "global::Opc.Ua.IEncodeable" :
|
|
// "global::Opc.Ua.IEncodeable?";
|
|
}
|
|
return "global::Opc.Ua.ExtensionObject";
|
|
case BasicDataType.Enumeration:
|
|
if (datatype.SymbolicId ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
return "int";
|
|
}
|
|
|
|
if (datatype.IsOptionSet)
|
|
{
|
|
return GetDotNetTypeName(
|
|
(DataTypeDesign)datatype.BaseTypeNode!,
|
|
targetNamespace,
|
|
namespaces,
|
|
nullable, // Should it always be non nullable?
|
|
useBuiltInWrapperTypes);
|
|
}
|
|
return datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
case BasicDataType.UserDefined:
|
|
string typeName = datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
|
|
if (datatype.IsEnumeration)
|
|
{
|
|
return typeName;
|
|
}
|
|
// All of these are always set to default type in properties when null
|
|
// is passed therefore we want to maintain non nullability no matter
|
|
return nullable != NullableAnnotation.Nullable ? typeName : typeName + "?";
|
|
}
|
|
if (!useBuiltInWrapperTypes)
|
|
{
|
|
return nullable == NullableAnnotation.NonNullable ? "object" : "object?";
|
|
}
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the type name for the data type with the provided value rank.
|
|
/// </summary>
|
|
public static string GetDotNetTypeName(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
NullableAnnotation nullable = NullableAnnotation.NonNullable,
|
|
bool useMatrixTypeInsteadOfVariant = false)
|
|
{
|
|
string typeName = dataType.GetDotNetTypeName(
|
|
targetNamespace,
|
|
namespaces,
|
|
// Make scalar optional nullable, but array/matrix elements non nullable
|
|
// TODO: Revisit this when we get rid of generated collections
|
|
valueRank == ValueRank.Scalar ? nullable : NullableAnnotation.NonNullable,
|
|
true);
|
|
if (typeName != null)
|
|
{
|
|
switch (valueRank)
|
|
{
|
|
case ValueRank.Scalar:
|
|
return typeName;
|
|
case ValueRank.Array:
|
|
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
|
|
case ValueRank.OneOrMoreDimensions when useMatrixTypeInsteadOfVariant:
|
|
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
|
|
}
|
|
}
|
|
return "global::Opc.Ua.Variant";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a template parameter is required.
|
|
/// </summary>
|
|
public static bool IsTemplateParameterRequired(
|
|
this DataTypeDesign dataType,
|
|
ValueRank valueRank)
|
|
{
|
|
// DiagnosticInfo is not a valid Variant value (OPC UA Part 6) and
|
|
// therefore has no IVariantBuilder<DiagnosticInfo>. A typed
|
|
// State<DiagnosticInfo> (scalar or ArrayOf/MatrixOf) cannot be
|
|
// emitted, so fall back to the non-generic, Variant-backed state.
|
|
// Mirrors the DiagnosticInfo exclusion in SupportsMatrixOf.
|
|
if (dataType.BasicDataType == BasicDataType.DiagnosticInfo)
|
|
{
|
|
return false;
|
|
}
|
|
if (dataType.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer &&
|
|
valueRank
|
|
is not ValueRank.OneOrMoreDimensions
|
|
and not ValueRank.ScalarOrOneDimension
|
|
and not ValueRank.ScalarOrArray)
|
|
{
|
|
return true;
|
|
}
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return true;
|
|
}
|
|
// Matrix-rank with a concrete data type that supports
|
|
// `MatrixOf<T>` is fully determinate (the generator produces
|
|
// `State<MatrixOf<T>>` rather than `State<T>`/`State<Variant>`),
|
|
// so the template parameter is in fact required to specialize
|
|
// the inherited State class.
|
|
if (valueRank == ValueRank.OneOrMoreDimensions &&
|
|
dataType.SupportsMatrixOf() &&
|
|
dataType.BasicDataType
|
|
is not BasicDataType.BaseDataType
|
|
and not BasicDataType.Number
|
|
and not BasicDataType.UInteger
|
|
and not BasicDataType.Integer)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is overridden instance.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static bool IsOverridden(this InstanceDesign instance)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
return instance.OveriddenNode != null &&
|
|
instance.ModellingRule != ModellingRule.None &&
|
|
instance.ModellingRule != ModellingRule.ExposesItsArray &&
|
|
instance.ModellingRule != ModellingRule.MandatoryPlaceholder &&
|
|
instance.ModellingRule != ModellingRule.OptionalPlaceholder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether a placeholder redeclares one its base type already
|
|
/// declares.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A placeholder is not overridden in the sense
|
|
/// <see cref="IsOverridden"/> means, because it names a slot rather than
|
|
/// a child, and merging it with the base declaration would be wrong. It
|
|
/// does however emit an Add method named after the slot, so a subtype
|
|
/// that redeclares the slot hides its base method and has to say so.
|
|
/// This method detects that redeclaration so the generated member can
|
|
/// be marked appropriately.
|
|
/// </remarks>
|
|
public static bool HidesBaseTypePlaceholder(this InstanceDesign instance)
|
|
{
|
|
if (instance is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(instance));
|
|
}
|
|
if (instance.ModellingRule is not ModellingRule.MandatoryPlaceholder and
|
|
not ModellingRule.OptionalPlaceholder)
|
|
{
|
|
return false;
|
|
}
|
|
// OveriddenNode is not linked for a placeholder, so the base chain
|
|
// is walked for a slot of the same name.
|
|
if (instance.Parent is not TypeDesign declaringType)
|
|
{
|
|
return false;
|
|
}
|
|
for (TypeDesign? baseType = declaringType.BaseTypeNode;
|
|
baseType != null;
|
|
baseType = baseType.BaseTypeNode)
|
|
{
|
|
InstanceDesign[]? children = baseType.Children?.Items;
|
|
if (children == null)
|
|
{
|
|
continue;
|
|
}
|
|
foreach (InstanceDesign child in children)
|
|
{
|
|
if (child.SymbolicName == instance.SymbolicName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the merged instance for an overriden node.
|
|
/// </summary>
|
|
public static InstanceDesign GetMergedInstance(this InstanceDesign instance)
|
|
{
|
|
for (NodeDesign? parent = instance.Parent; parent != null; parent = parent.Parent)
|
|
{
|
|
if (parent.Parent == null && parent.Hierarchy != null)
|
|
{
|
|
// The hierarchy is keyed by the path relative to the root, so
|
|
// the root's own symbolic name has to come off the front.
|
|
// Strip that exact prefix rather than everything up to the
|
|
// first '_', which mangles the path whenever the root type's
|
|
// name itself contains an underscore.
|
|
string relativePath = instance.SymbolicId.Name;
|
|
string rootPrefix = parent.SymbolicId?.Name + "_";
|
|
|
|
if (rootPrefix.Length > 1 &&
|
|
relativePath.StartsWith(rootPrefix, StringComparison.Ordinal))
|
|
{
|
|
relativePath = relativePath[rootPrefix.Length..];
|
|
}
|
|
else
|
|
{
|
|
int index = relativePath.IndexOf('_', StringComparison.Ordinal);
|
|
|
|
if (index != -1)
|
|
{
|
|
relativePath = relativePath[(index + 1)..];
|
|
}
|
|
}
|
|
if (parent.Hierarchy.Nodes.TryGetValue(relativePath,
|
|
out HierarchyNode? hierarchyNode) &&
|
|
hierarchyNode.Instance is InstanceDesign instanceDesign)
|
|
{
|
|
return instanceDesign;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a name qualified with a namespace prefix.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static string GetPrefixedName(this XmlQualifiedName qname, List<string> namespaceUris)
|
|
{
|
|
if (qname.IsNull())
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
if (qname.Namespace == Namespaces.OpcUaBuiltInTypes)
|
|
{
|
|
return CoreUtils.Format("ua:{0}", qname.Name);
|
|
}
|
|
|
|
if (namespaceUris is null)
|
|
|
|
{
|
|
throw new ArgumentNullException(nameof(namespaceUris));
|
|
}
|
|
int index = namespaceUris.IndexOf(qname.Namespace);
|
|
|
|
if (index > 0)
|
|
{
|
|
return CoreUtils.Format("s{0}:{1}", index - 1, qname.Name);
|
|
}
|
|
|
|
return qname.Name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type to use for the value of a variable or the argument of a method.
|
|
/// </summary>
|
|
public static string GetBinaryDataType(
|
|
this DataTypeDesign? dataType,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
switch (dataType!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "opc:Boolean";
|
|
case BasicDataType.SByte:
|
|
return "opc:SByte";
|
|
case BasicDataType.Byte:
|
|
return "opc:Byte";
|
|
case BasicDataType.Int16:
|
|
return "opc:Int16";
|
|
case BasicDataType.UInt16:
|
|
return "opc:UInt16";
|
|
case BasicDataType.Int32:
|
|
return "opc:Int32";
|
|
case BasicDataType.UInt32:
|
|
return "opc:UInt32";
|
|
case BasicDataType.Int64:
|
|
return "opc:Int64";
|
|
case BasicDataType.UInt64:
|
|
return "opc:UInt64";
|
|
case BasicDataType.Float:
|
|
return "opc:Float";
|
|
case BasicDataType.Double:
|
|
return "opc:Double";
|
|
case BasicDataType.String:
|
|
return "opc:CharArray";
|
|
case BasicDataType.DateTime:
|
|
return "opc:DateTime";
|
|
case BasicDataType.Guid:
|
|
return "opc:Guid";
|
|
case BasicDataType.ByteString:
|
|
return "opc:ByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:DiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:DataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:Variant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetBinaryDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("opc:Int32");
|
|
}
|
|
|
|
string? prefix = "tns";
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(
|
|
namespaces,
|
|
dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
return CoreUtils.Format("{0}:{1}", prefix, dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the data type to use for the value of a variable or the argument of a method.
|
|
/// </summary>
|
|
public static string GetXmlDataType(
|
|
this DataTypeDesign? dataType,
|
|
ValueRank valueRank,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (valueRank != ValueRank.Scalar)
|
|
{
|
|
switch (dataType!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "ua:ListOfBoolean";
|
|
case BasicDataType.SByte:
|
|
return "ua:ListOfSByte";
|
|
case BasicDataType.Byte:
|
|
return "ua:ListOfByte";
|
|
case BasicDataType.Int16:
|
|
return "ua:ListOfInt16";
|
|
case BasicDataType.UInt16:
|
|
return "ua:ListOfUInt16";
|
|
case BasicDataType.Int32:
|
|
return "ua:ListOfInt32";
|
|
case BasicDataType.UInt32:
|
|
return "ua:ListOfUInt32";
|
|
case BasicDataType.Int64:
|
|
return "ua:ListOfInt64";
|
|
case BasicDataType.UInt64:
|
|
return "ua:ListOfUInt64";
|
|
case BasicDataType.Float:
|
|
return "ua:ListOfFloat";
|
|
case BasicDataType.Double:
|
|
return "ua:ListOfDouble";
|
|
case BasicDataType.String:
|
|
return "ua:ListOfString";
|
|
case BasicDataType.DateTime:
|
|
return "ua:ListOfDateTime";
|
|
case BasicDataType.Guid:
|
|
return "ua:ListOfGuid";
|
|
case BasicDataType.ByteString:
|
|
return "ua:ListOfByteString";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:ListOfXmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:ListOfNodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ListOfExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:ListOfStatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:ListOfDiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:ListOfQualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:ListOfLocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:ListOfDataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:ListOfVariant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ListOfExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("ua:ListOfInt32");
|
|
}
|
|
|
|
string? prefix = "tns";
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
if (dataType.SymbolicName.Name == "Enumeration")
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("ua:ListOfInt32");
|
|
}
|
|
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
|
|
return CoreUtils.Format("{0}:ListOf{1}", prefix, dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
switch (dataType!.BasicDataType)
|
|
{
|
|
case BasicDataType.Boolean:
|
|
return "xs:boolean";
|
|
case BasicDataType.SByte:
|
|
return "xs:byte";
|
|
case BasicDataType.Byte:
|
|
return "xs:unsignedByte";
|
|
case BasicDataType.Int16:
|
|
return "xs:short";
|
|
case BasicDataType.UInt16:
|
|
return "xs:unsignedShort";
|
|
case BasicDataType.Int32:
|
|
return "xs:int";
|
|
case BasicDataType.UInt32:
|
|
return "xs:unsignedInt";
|
|
case BasicDataType.Int64:
|
|
return "xs:long";
|
|
case BasicDataType.UInt64:
|
|
return "xs:unsignedLong";
|
|
case BasicDataType.Float:
|
|
return "xs:float";
|
|
case BasicDataType.Double:
|
|
return "xs:double";
|
|
case BasicDataType.String:
|
|
return "xs:string";
|
|
case BasicDataType.DateTime:
|
|
return "xs:dateTime";
|
|
case BasicDataType.Guid:
|
|
return "ua:Guid";
|
|
case BasicDataType.ByteString:
|
|
return "xs:base64Binary";
|
|
case BasicDataType.XmlElement:
|
|
return "ua:XmlElement";
|
|
case BasicDataType.NodeId:
|
|
return "ua:NodeId";
|
|
case BasicDataType.ExpandedNodeId:
|
|
return "ua:ExpandedNodeId";
|
|
case BasicDataType.StatusCode:
|
|
return "ua:StatusCode";
|
|
case BasicDataType.DiagnosticInfo:
|
|
return "ua:DiagnosticInfo";
|
|
case BasicDataType.QualifiedName:
|
|
return "ua:QualifiedName";
|
|
case BasicDataType.LocalizedText:
|
|
return "ua:LocalizedText";
|
|
case BasicDataType.DataValue:
|
|
return "ua:DataValue";
|
|
case BasicDataType.Number:
|
|
case BasicDataType.Integer:
|
|
case BasicDataType.UInteger:
|
|
case BasicDataType.BaseDataType:
|
|
return "ua:Variant";
|
|
default:
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Structure", Namespaces.OpcUa))
|
|
{
|
|
return CoreUtils.Format("ua:ExtensionObject");
|
|
}
|
|
|
|
if (dataType.SymbolicName ==
|
|
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("xs:int");
|
|
}
|
|
|
|
string? prefix = null;
|
|
|
|
if (dataType.SymbolicName.Namespace != targetNamespace)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
|
|
{
|
|
if (dataType.SymbolicName.Name == "Enumeration")
|
|
{
|
|
if (dataType.IsOptionSet)
|
|
{
|
|
return GetXmlDataType(
|
|
(DataTypeDesign)dataType.BaseTypeNode!,
|
|
valueRank,
|
|
targetNamespace,
|
|
namespaces);
|
|
}
|
|
|
|
return CoreUtils.Format("xs:int");
|
|
}
|
|
|
|
prefix = "ua";
|
|
}
|
|
else
|
|
{
|
|
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
|
|
}
|
|
}
|
|
|
|
return CoreUtils.Format("{0}:{1}", prefix ?? "tns", dataType.SymbolicName.Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant for the namespace uri.
|
|
/// </summary>
|
|
public static Namespace? GetNamespace(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaces != null)
|
|
{
|
|
foreach (Namespace ns in namespaces)
|
|
{
|
|
if (ns.Value == namespaceUri)
|
|
{
|
|
return ns;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a constant for the namespace uri.
|
|
/// </summary>
|
|
public static string? GetConstantForXmlNamespace(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
Namespace? ns = GetNamespace(namespaces, namespaceUri);
|
|
if (ns != null)
|
|
{
|
|
// The "...Xsd" companion constant only exists when the XML
|
|
// namespace differs from the namespace URI - ConstantsGenerator
|
|
// emits it under exactly that condition. Referencing it whenever
|
|
// XmlNamespace is merely non-empty names a constant that was
|
|
// never declared (CS0117 in the generated sources).
|
|
if (!string.IsNullOrEmpty(ns.XmlNamespace) &&
|
|
!string.Equals(ns.XmlNamespace, ns.Value, StringComparison.Ordinal))
|
|
{
|
|
return CoreUtils.Format("{1}.Namespaces.{0}Xsd", ns.Name, ns.Prefix);
|
|
}
|
|
|
|
return CoreUtils.Format("{1}.Namespaces.{0}", ns.Name, ns.Prefix);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the XML prefix for the specified namespace.
|
|
/// </summary>
|
|
public static string? GetXmlNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
|
|
{
|
|
if (namespaceUri == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (namespaces != null)
|
|
{
|
|
for (int ii = 0; ii < namespaces.Length; ii++)
|
|
{
|
|
if (namespaces[ii].Value == namespaceUri)
|
|
{
|
|
if (string.IsNullOrEmpty(namespaces[ii].XmlPrefix))
|
|
{
|
|
return CoreUtils.Format("s{0}", ii);
|
|
}
|
|
|
|
return namespaces[ii].XmlPrefix;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static bool IsMethodTypeDesign([NotNullWhen(true)] this NodeDesign? node)
|
|
{
|
|
if (node?.SymbolicId is null)
|
|
{
|
|
return false;
|
|
}
|
|
if (node is not MethodDesign)
|
|
|
|
{
|
|
return false;
|
|
}
|
|
string symbol = node.SymbolicId.Name;
|
|
|
|
int index = symbol.IndexOf('_', StringComparison.Ordinal);
|
|
|
|
if (index > 0)
|
|
|
|
{
|
|
symbol = symbol[..index];
|
|
}
|
|
return symbol.EndsWith("MethodType", StringComparison.Ordinal);
|
|
}
|
|
|
|
public static bool IsPartOfOpcUaTypesLibrary(this DataTypeDesign dataType)
|
|
{
|
|
if (dataType != null &&
|
|
dataType.SymbolicId.Namespace == Namespaces.OpcUa)
|
|
{
|
|
switch (dataType.NumericId)
|
|
{
|
|
case DataTypes.AccessRestrictionType:
|
|
case DataTypes.ReferenceDescription:
|
|
case DataTypes.AttributeWriteMask:
|
|
case DataTypes.Argument:
|
|
case DataTypes.IdType:
|
|
case DataTypes.RolePermissionType:
|
|
case DataTypes.PermissionType:
|
|
case DataTypes.ViewDescription:
|
|
case DataTypes.BrowseDescription:
|
|
case DataTypes.StructureDefinition:
|
|
case DataTypes.StructureType:
|
|
case DataTypes.StructureField:
|
|
case DataTypes.InstanceNode:
|
|
case DataTypes.ReferenceTypeNode:
|
|
case DataTypes.ReferenceNode:
|
|
case DataTypes.DataTypeDefinition:
|
|
case DataTypes.EnumDefinition:
|
|
case DataTypes.EnumField:
|
|
case DataTypes.EnumValueType:
|
|
case DataTypes.RelativePath:
|
|
case DataTypes.BrowseDirection:
|
|
case DataTypes.RelativePathElement:
|
|
case DataTypes.NodeClass:
|
|
case DataTypes.Node:
|
|
case DataTypes.ViewNode:
|
|
case DataTypes.ObjectNode:
|
|
case DataTypes.MethodNode:
|
|
case DataTypes.TypeNode:
|
|
case DataTypes.ObjectTypeNode:
|
|
case DataTypes.DataTypeNode:
|
|
case DataTypes.VariableTypeNode:
|
|
case DataTypes.VariableNode:
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static Export.ReleaseStatus ToNodeSetReleaseStatus(
|
|
this ReleaseStatus releaseStatus)
|
|
{
|
|
switch (releaseStatus)
|
|
{
|
|
case ReleaseStatus.Deprecated:
|
|
return Export.ReleaseStatus.Deprecated;
|
|
case ReleaseStatus.RC:
|
|
case ReleaseStatus.Draft:
|
|
return Export.ReleaseStatus.Draft;
|
|
default:
|
|
return Export.ReleaseStatus.Released;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the access level enumeration onto a code string.
|
|
/// </summary>
|
|
public static string GetAccessLevelAsCode(this AccessLevel accessLevel)
|
|
{
|
|
return accessLevel switch
|
|
{
|
|
AccessLevel.Read => "global::Opc.Ua.AccessLevels.CurrentRead",
|
|
AccessLevel.Write => "global::Opc.Ua.AccessLevels.CurrentWrite",
|
|
AccessLevel.ReadWrite => "global::Opc.Ua.AccessLevels.CurrentReadOrWrite",
|
|
AccessLevel.HistoryRead => "global::Opc.Ua.AccessLevels.HistoryRead",
|
|
AccessLevel.HistoryWrite => "global::Opc.Ua.AccessLevels.HistoryWrite",
|
|
AccessLevel.HistoryReadWrite => "global::Opc.Ua.AccessLevels.HistoryReadOrWrite",
|
|
_ => "global::Opc.Ua.AccessLevels.None"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the AccessLevel of a variable onto code. NodeSet2-sourced
|
|
/// designs carry the verbatim bitmask, which is preferred because
|
|
/// the ModelDesign <see cref="AccessLevel"/> enumeration cannot
|
|
/// represent combinations such as <c>CurrentRead | HistoryRead</c>.
|
|
/// </summary>
|
|
public static string GetAccessLevelAsCode(this VariableDesign? variable)
|
|
{
|
|
uint? rawAccessLevel = variable?.RawAccessLevel;
|
|
return rawAccessLevel != null
|
|
? GetAccessLevelBitsAsCode(rawAccessLevel.Value)
|
|
: GetAccessLevelAsCode(variable?.AccessLevel ?? AccessLevel.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the UserAccessLevel of a variable onto code. NodeSet2-sourced
|
|
/// designs carry the verbatim UserAccessLevel bitmask when the
|
|
/// attribute is explicitly present and it is preferred; otherwise the
|
|
/// UserAccessLevel is derived from the AccessLevel, matching the
|
|
/// runtime NodeSet2 importer in <c>UANodeSetHelpers</c>.
|
|
/// </summary>
|
|
public static string GetUserAccessLevelAsCode(this VariableDesign variable)
|
|
{
|
|
uint? rawUserAccessLevel = variable?.RawUserAccessLevel;
|
|
return rawUserAccessLevel != null
|
|
? GetAccessLevelBitsAsCode(rawUserAccessLevel.Value)
|
|
: GetAccessLevelAsCode(variable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a verbatim OPC UA AccessLevel bitmask onto code, emitting
|
|
/// the named <c>AccessLevels</c> constants for every set bit.
|
|
/// </summary>
|
|
private static string GetAccessLevelBitsAsCode(uint accessLevel)
|
|
{
|
|
byte bits = (byte)accessLevel;
|
|
if (bits == 0)
|
|
{
|
|
return "global::Opc.Ua.AccessLevels.None";
|
|
}
|
|
|
|
var names = new List<string>();
|
|
AppendBit(names, bits, 0x1, "CurrentRead");
|
|
AppendBit(names, bits, 0x2, "CurrentWrite");
|
|
AppendBit(names, bits, 0x4, "HistoryRead");
|
|
AppendBit(names, bits, 0x8, "HistoryWrite");
|
|
AppendBit(names, bits, 0x10, "SemanticChange");
|
|
AppendBit(names, bits, 0x20, "StatusWrite");
|
|
AppendBit(names, bits, 0x40, "TimestampWrite");
|
|
|
|
if (names.Count == 0)
|
|
{
|
|
return CoreUtils.Format("(byte)0x{0:X2}", bits);
|
|
}
|
|
if (names.Count == 1)
|
|
{
|
|
return names[0];
|
|
}
|
|
return CoreUtils.Format("(byte)({0})", string.Join(" | ", names));
|
|
}
|
|
|
|
private static void AppendBit(List<string> names, byte bits, byte mask, string name)
|
|
{
|
|
if ((bits & mask) != 0)
|
|
{
|
|
names.Add("global::Opc.Ua.AccessLevels." + name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a permission's namespace-qualified wire identity without requiring a generated role node.
|
|
/// </summary>
|
|
/// <exception cref="InvalidDataException">
|
|
/// The permission does not identify a representable namespace-qualified role.
|
|
/// </exception>
|
|
internal static ExpandedNodeId ResolveRoleId(this RolePermission permission, IModelDesign model)
|
|
{
|
|
if (permission.RoleId != null)
|
|
{
|
|
if (ExpandedNodeId.TryParse(permission.RoleId, out ExpandedNodeId roleId) &&
|
|
!roleId.IsNull &&
|
|
roleId.ServerIndex == 0 &&
|
|
(roleId.NamespaceIndex == 0 || !string.IsNullOrEmpty(roleId.NamespaceUri)) &&
|
|
(roleId.NamespaceUri != Namespaces.OpcUa || !roleId.InnerNodeId.IsNull))
|
|
{
|
|
return roleId.WithNamespaceUri(roleId.NamespaceUri ?? Namespaces.OpcUa);
|
|
}
|
|
}
|
|
else if (permission.Role != null &&
|
|
model.TryFindNode(permission.Role, permission.Role.Name, "RoleType", out NodeDesign? role))
|
|
{
|
|
uint? numericId = role.FindNumericIdentifier();
|
|
NodeId roleId = role.GetIdentifier() switch
|
|
{
|
|
uint numeric => new NodeId(numeric),
|
|
string text => new NodeId(text, 0),
|
|
Guid guid => new NodeId(guid),
|
|
ByteString opaque => new NodeId(opaque),
|
|
_ when numericId.HasValue => new NodeId(numericId.Value),
|
|
_ => new NodeId(role.SymbolicId?.Name ?? string.Empty, 0)
|
|
};
|
|
if ((role.HasIdentifier() || numericId.HasValue || !string.IsNullOrEmpty(role.SymbolicId?.Name)) &&
|
|
!string.IsNullOrEmpty(role.SymbolicId?.Namespace) &&
|
|
(!roleId.IsNull || role.SymbolicId.Namespace != Namespaces.OpcUa))
|
|
{
|
|
return ((ExpandedNodeId)roleId).WithNamespaceUri(role.SymbolicId.Namespace);
|
|
}
|
|
}
|
|
throw new InvalidDataException(
|
|
$"RolePermissions cannot represent role '{permission.RoleId ?? permission.Role?.ToString()}'.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats the identifier kind even for an empty identifier in a nonzero namespace.
|
|
/// </summary>
|
|
internal static string FormatRoleId(this ExpandedNodeId roleId)
|
|
{
|
|
var text = new StringBuilder();
|
|
NodeId identifier = roleId.InnerNodeId;
|
|
ExpandedNodeId.Format(
|
|
CultureInfo.InvariantCulture,
|
|
text,
|
|
identifier.IdentifierAsString,
|
|
identifier.IdType,
|
|
0,
|
|
roleId.NamespaceUri,
|
|
0);
|
|
return text.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the WriteAccess (WriteMask) of a node onto code. The verbatim
|
|
/// OPC UA WriteMask bitmask is emitted so that attributes advertised as
|
|
/// writable in the NodeSet (for example DisplayName and Description)
|
|
/// stay writable at runtime instead of defaulting to None.
|
|
/// </summary>
|
|
public static string GetWriteMaskAsCode(this NodeDesign node)
|
|
{
|
|
return GetWriteMaskAsCode(node?.WriteAccess ?? 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an explicit user mask independently, retaining the fallback for unconfigured masks.
|
|
/// </summary>
|
|
public static string GetUserWriteMaskAsCode(this NodeDesign node)
|
|
{
|
|
return GetWriteMaskAsCode(node?.UserWriteAccessSpecified == true
|
|
? node.UserWriteAccess
|
|
: node?.WriteAccess ?? 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats an attribute write mask without changing its bits.
|
|
/// </summary>
|
|
private static string GetWriteMaskAsCode(uint writeMask)
|
|
{
|
|
if (writeMask == 0)
|
|
{
|
|
return "global::Opc.Ua.AttributeWriteMask.None";
|
|
}
|
|
return CoreUtils.Format(
|
|
"(global::Opc.Ua.AttributeWriteMask){0}",
|
|
writeMask);
|
|
}
|
|
|
|
public static string GetLocalizedTextAsCode(this string localizedText)
|
|
{
|
|
return GetLocalizedTextAsCode(new LocalizedText
|
|
{
|
|
Value = localizedText
|
|
}, noAutoGen: false);
|
|
}
|
|
|
|
public static string GetLocalizedTextAsCode(this LocalizedText? localizedText, bool noAutoGen = false)
|
|
{
|
|
if (localizedText?.Value != null && (!noAutoGen || !localizedText.IsAutogenerated))
|
|
{
|
|
return CoreUtils.Format(
|
|
"new global::Opc.Ua.LocalizedText({0}, string.Empty, {1})",
|
|
localizedText.Key.AsStringLiteral(),
|
|
localizedText.Value.Trim().AsStringLiteral());
|
|
}
|
|
return "global::Opc.Ua.LocalizedText.Null";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps the array dimensions onto code that creates the array.
|
|
/// </summary>
|
|
public static string? GetArrayDimensionsAsCode(this ValueRank valueRank, string arrayDimensions)
|
|
{
|
|
// Part 3 5.6.2: ArrayDimensions is null unless the ValueRank names an
|
|
// array of a specific dimension. ModelDesign's ValueRank is the XSD
|
|
// enum (Scalar = 0 ... Any = 5), not the numeric OPC UA rank, so a
|
|
// "< 0" test never fired and a scalar declared with
|
|
// ArrayDimensions="0" (as NodeSet exporters write it) got them.
|
|
if (valueRank is ValueRank.Scalar or
|
|
ValueRank.ScalarOrArray or
|
|
ValueRank.ScalarOrOneDimension or
|
|
ValueRank.Any)
|
|
{
|
|
return null;
|
|
}
|
|
if (string.IsNullOrEmpty(arrayDimensions))
|
|
{
|
|
if (valueRank == ValueRank.Array)
|
|
{
|
|
return "new uint[] { 0 }";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
var tokens = arrayDimensions
|
|
.Split([','], StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(t => t.Trim())
|
|
.Where(t => t.Length > 0)
|
|
.ToList();
|
|
|
|
if (tokens.Count == 0)
|
|
|
|
{
|
|
return null;
|
|
}
|
|
return CoreUtils.Format(
|
|
"new uint[] {{ {0} }}",
|
|
string.Join(", ", tokens.Select(t =>
|
|
{
|
|
if (uint.TryParse(t, out uint val))
|
|
{
|
|
return val.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
return "0";
|
|
})));
|
|
}
|
|
|
|
public static string? GetAccessRestrictionsAsCode(
|
|
this AccessRestrictions restrictions,
|
|
bool restrictionsSpecified)
|
|
{
|
|
if (!restrictionsSpecified)
|
|
{
|
|
return null;
|
|
}
|
|
return restrictions switch
|
|
{
|
|
AccessRestrictions.None =>
|
|
"global::Opc.Ua.AccessRestrictionType.None",
|
|
AccessRestrictions.ApplyToBrowse =>
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SigningRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired",
|
|
AccessRestrictions.EncryptionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired",
|
|
AccessRestrictions.SessionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SessionWithSigningRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SessionWithEncryptionRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
|
|
AccessRestrictions.SigningAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.EncryptionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionWithSigningAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
AccessRestrictions.SessionWithEncryptionAndApplyToBrowseRequired =>
|
|
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
|
|
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
public static string GetPermissionTypeAsCode(this Permissions[] permissions)
|
|
{
|
|
if (permissions == null || permissions.Length == 0)
|
|
{
|
|
return "global::Opc.Ua.PermissionType.None";
|
|
}
|
|
var parts = new HashSet<string>();
|
|
foreach (Permissions p in permissions)
|
|
{
|
|
switch (p)
|
|
{
|
|
case Permissions.All:
|
|
AddPermissionStrings(Permissions.WriteAttribute, parts);
|
|
AddPermissionStrings(Permissions.WriteRolePermissions, parts);
|
|
AddPermissionStrings(Permissions.WriteHistorizing, parts);
|
|
AddPermissionStrings(Permissions.Write, parts);
|
|
AddPermissionStrings(Permissions.InsertHistory, parts);
|
|
AddPermissionStrings(Permissions.ModifyHistory, parts);
|
|
AddPermissionStrings(Permissions.DeleteHistory, parts);
|
|
AddPermissionStrings(Permissions.Call, parts);
|
|
AddPermissionStrings(Permissions.AddReference, parts);
|
|
AddPermissionStrings(Permissions.RemoveReference, parts);
|
|
AddPermissionStrings(Permissions.DeleteNode, parts);
|
|
AddPermissionStrings(Permissions.AddNode, parts);
|
|
goto case Permissions.AllRead;
|
|
case Permissions.AllRead:
|
|
AddPermissionStrings(Permissions.Browse, parts);
|
|
AddPermissionStrings(Permissions.Read, parts);
|
|
AddPermissionStrings(Permissions.ReadHistory, parts);
|
|
AddPermissionStrings(Permissions.ReceiveEvents, parts);
|
|
AddPermissionStrings(Permissions.ReadRolePermissions, parts);
|
|
break;
|
|
default:
|
|
AddPermissionStrings(p, parts);
|
|
break;
|
|
}
|
|
}
|
|
return parts.Count > 0 ?
|
|
string.Join(" | ", parts) :
|
|
"global::Opc.Ua.PermissionType.None";
|
|
|
|
static void AddPermissionStrings(Permissions p, HashSet<string> parts)
|
|
{
|
|
string? part = p switch
|
|
{
|
|
Permissions.Browse =>
|
|
"global::Opc.Ua.PermissionType.Browse",
|
|
Permissions.Read =>
|
|
"global::Opc.Ua.PermissionType.Read",
|
|
Permissions.Write =>
|
|
"global::Opc.Ua.PermissionType.Write",
|
|
Permissions.Call =>
|
|
"global::Opc.Ua.PermissionType.Call",
|
|
Permissions.ReadHistory =>
|
|
"global::Opc.Ua.PermissionType.ReadHistory",
|
|
Permissions.InsertHistory =>
|
|
"global::Opc.Ua.PermissionType.InsertHistory",
|
|
Permissions.ModifyHistory =>
|
|
"global::Opc.Ua.PermissionType.ModifyHistory",
|
|
Permissions.DeleteHistory =>
|
|
"global::Opc.Ua.PermissionType.DeleteHistory",
|
|
Permissions.ReceiveEvents =>
|
|
"global::Opc.Ua.PermissionType.ReceiveEvents",
|
|
Permissions.AddNode =>
|
|
"global::Opc.Ua.PermissionType.AddNode",
|
|
Permissions.DeleteNode =>
|
|
"global::Opc.Ua.PermissionType.DeleteNode",
|
|
Permissions.AddReference =>
|
|
"global::Opc.Ua.PermissionType.AddReference",
|
|
Permissions.RemoveReference =>
|
|
"global::Opc.Ua.PermissionType.RemoveReference",
|
|
Permissions.ReadRolePermissions =>
|
|
"global::Opc.Ua.PermissionType.ReadRolePermissions",
|
|
Permissions.WriteRolePermissions =>
|
|
"global::Opc.Ua.PermissionType.WriteRolePermissions",
|
|
Permissions.WriteAttribute =>
|
|
"global::Opc.Ua.PermissionType.WriteAttribute",
|
|
Permissions.WriteHistorizing =>
|
|
"global::Opc.Ua.PermissionType.WriteHistorizing",
|
|
_ => null
|
|
};
|
|
if (part != null)
|
|
{
|
|
parts.Add(part);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the identifier assigned to the node as one of the four
|
|
/// identifier types defined by OPC 10000-3 5.2.2, or <c>null</c> when
|
|
/// the node does not carry an explicit identifier.
|
|
/// </summary>
|
|
public static object? GetIdentifier(this NodeDesign? node)
|
|
{
|
|
if (node == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
return node.NumericId;
|
|
}
|
|
if (node.StringId != null)
|
|
{
|
|
return node.StringId;
|
|
}
|
|
if (node.GuidIdSpecified)
|
|
{
|
|
return node.GuidId;
|
|
}
|
|
if (node.OpaqueId != null)
|
|
{
|
|
return ByteString.From(node.OpaqueId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns the identifier of the node, clearing the identifiers of all
|
|
/// other identifier types. Passing <c>null</c> clears the identifier.
|
|
/// </summary>
|
|
public static void SetIdentifier(this NodeDesign? node, object? identifier)
|
|
{
|
|
if (node == null)
|
|
{
|
|
return;
|
|
}
|
|
node.NumericId = 0;
|
|
node.NumericIdSpecified = false;
|
|
node.StringId = null;
|
|
node.GuidId = Guid.Empty;
|
|
node.GuidIdSpecified = false;
|
|
node.OpaqueId = null;
|
|
|
|
switch (identifier)
|
|
{
|
|
case uint numericId:
|
|
node.NumericId = numericId;
|
|
node.NumericIdSpecified = true;
|
|
break;
|
|
case string stringId:
|
|
node.StringId = stringId;
|
|
break;
|
|
case Guid guidId:
|
|
node.GuidId = guidId;
|
|
node.GuidIdSpecified = true;
|
|
break;
|
|
case ByteString opaqueId:
|
|
node.OpaqueId = opaqueId.ToArray();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when the node carries an explicit identifier of any of
|
|
/// the four identifier types defined by OPC 10000-3 5.2.2.
|
|
/// </summary>
|
|
public static bool HasIdentifier(this NodeDesign node)
|
|
{
|
|
return node.GetIdentifier() != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when the node carries a Guid or Opaque identifier.
|
|
/// These cannot be emitted as a C# <c>const</c> and are not part of the
|
|
/// generated identifier reflection tables.
|
|
/// </summary>
|
|
public static bool HasNonConstantIdentifier(this NodeDesign node)
|
|
{
|
|
return node != null &&
|
|
!node.NumericIdSpecified &&
|
|
string.IsNullOrEmpty(node.StringId) &&
|
|
(node.GuidIdSpecified || node.OpaqueId != null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the C# literal for an identifier together with the C# type
|
|
/// the literal has.
|
|
/// </summary>
|
|
public static string? GetIdentifierAsCode(object identifier, out string? type)
|
|
{
|
|
switch (identifier)
|
|
{
|
|
case uint numericId:
|
|
type = "uint";
|
|
return CoreUtils.Format("{0}u", numericId);
|
|
case string stringId:
|
|
type = "string";
|
|
return stringId.AsStringLiteral();
|
|
case Guid guidId:
|
|
type = "global::System.Guid";
|
|
return CoreUtils.Format(
|
|
"new global::System.Guid({0})",
|
|
guidId.ToString("D", CultureInfo.InvariantCulture).AsStringLiteral());
|
|
case ByteString opaqueId:
|
|
type = "global::Opc.Ua.ByteString";
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.ByteString.FromBase64({0})",
|
|
opaqueId.ToBase64().AsStringLiteral());
|
|
default:
|
|
type = null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static string GetNodeIdAsCode(
|
|
this NodeDesign? node,
|
|
Namespace[] namespaces,
|
|
string namespaceTableVariable)
|
|
{
|
|
string? identifier;
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", node.NumericId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(node.StringId))
|
|
{
|
|
identifier = node.StringId.AsStringLiteral();
|
|
}
|
|
else if (node.HasNonConstantIdentifier())
|
|
{
|
|
identifier = GetIdentifierAsCode(node.GetIdentifier()!, out _);
|
|
}
|
|
else
|
|
{
|
|
uint? numericId = FindNumericIdentifier(node);
|
|
if (numericId.HasValue)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", numericId.Value);
|
|
}
|
|
else
|
|
{
|
|
if (string.IsNullOrEmpty(node.SymbolicId.Name))
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
identifier = node.SymbolicId.Name.AsStringLiteral();
|
|
}
|
|
}
|
|
return CoreUtils.Format(
|
|
"global::Opc.Ua.NodeId.Create({0}, {1}, {2})",
|
|
identifier,
|
|
namespaces.GetNamespaceUriAsCode(
|
|
node.SymbolicId.Namespace),
|
|
namespaceTableVariable);
|
|
}
|
|
|
|
public static string GetExpandedNodeIdAsCode(
|
|
this NodeDesign node,
|
|
Namespace[] namespaces)
|
|
{
|
|
string? identifier;
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.ExpandedNodeId.Null";
|
|
}
|
|
if (node.NumericIdSpecified)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", node.NumericId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(node.StringId))
|
|
{
|
|
identifier = node.StringId.AsStringLiteral();
|
|
}
|
|
else if (node.HasNonConstantIdentifier())
|
|
{
|
|
identifier = GetIdentifierAsCode(node.GetIdentifier()!, out _);
|
|
}
|
|
else
|
|
{
|
|
uint? numericId = FindNumericIdentifier(node);
|
|
if (numericId.HasValue)
|
|
{
|
|
identifier = CoreUtils.Format("{0}u", numericId.Value);
|
|
}
|
|
else
|
|
{
|
|
if (string.IsNullOrEmpty(node.SymbolicId.Name))
|
|
{
|
|
return "global::Opc.Ua.ExpandedNodeId.Null";
|
|
}
|
|
identifier = node.SymbolicId.Name.AsStringLiteral();
|
|
}
|
|
}
|
|
return CoreUtils.Format(
|
|
"new global::Opc.Ua.ExpandedNodeId({0}, {1})",
|
|
identifier,
|
|
namespaces.GetNamespaceUriAsCode(
|
|
node.SymbolicId.Namespace));
|
|
}
|
|
|
|
public static uint? FindNumericIdentifier(this NodeDesign node)
|
|
{
|
|
uint? numericId = node.NumericIdSpecified ? node.NumericId : null;
|
|
for (NodeDesign? parent = node.Parent;
|
|
!numericId.HasValue && parent != null;
|
|
parent = parent.Parent)
|
|
{
|
|
if (parent.Hierarchy == null)
|
|
{
|
|
continue;
|
|
}
|
|
string browsePath = node.SymbolicId.Name;
|
|
string parentPath = parent.SymbolicId.Name;
|
|
if (browsePath.StartsWith(parentPath, StringComparison.InvariantCulture) &&
|
|
browsePath[parentPath.Length] == NodeDesign.PathChar)
|
|
{
|
|
browsePath = browsePath[(parentPath.Length + 1)..];
|
|
}
|
|
|
|
if (parent.Hierarchy.Nodes.TryGetValue(browsePath, out HierarchyNode? instance) &&
|
|
instance.Instance!.NumericId != 0)
|
|
{
|
|
numericId = instance.Instance.NumericId;
|
|
}
|
|
}
|
|
return numericId;
|
|
}
|
|
|
|
public static string GetNodeIdConstant(
|
|
this IModelDesign design,
|
|
XmlQualifiedName? symbolidId,
|
|
string type,
|
|
string namespaceTableVariable)
|
|
{
|
|
if (symbolidId == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
NodeDesign node = design.FindNode(
|
|
symbolidId,
|
|
symbolidId.Name,
|
|
type);
|
|
if (node == null)
|
|
{
|
|
return "global::Opc.Ua.NodeId.Null";
|
|
}
|
|
return node.GetNodeIdAsCode(design.Namespaces, namespaceTableVariable);
|
|
}
|
|
|
|
public static void SetDefaultValue(
|
|
this VariableTypeDesign node,
|
|
object value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (value != null)
|
|
{
|
|
node.DefaultValue = AsXmlElement(value, context);
|
|
node.DecodedValue = value;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
else
|
|
{
|
|
node.DefaultValue = null;
|
|
node.DecodedValue = null;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
}
|
|
|
|
public static void SetDefaultValue(
|
|
this VariableDesign node,
|
|
object? value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (value != null)
|
|
{
|
|
node.DefaultValue = AsXmlElement(value, context);
|
|
node.DecodedValue = value;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
else
|
|
{
|
|
node.DefaultValue = null;
|
|
node.DecodedValue = null;
|
|
node.DecodedValueNamespaceUris = null;
|
|
}
|
|
}
|
|
|
|
private static System.Xml.XmlElement? AsXmlElement(
|
|
object value,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (VariantHelper.TryCastFromWithReflectionFallback(value, out Variant variant) &&
|
|
!variant.IsNull)
|
|
{
|
|
using var encoder = new XmlEncoder(context);
|
|
encoder.WriteVariantValue(null, variant);
|
|
var xmlDoc = new XmlDocument();
|
|
xmlDoc.LoadInnerXml(encoder.CloseAndReturnText()!);
|
|
return xmlDoc.DocumentElement;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static bool IsBasicDataType(
|
|
this DataTypeDesign dataType,
|
|
out BasicDataType basicDataType)
|
|
{
|
|
if (dataType.SymbolicName.Namespace == "http://opcfoundation.org/UA/")
|
|
{
|
|
#if NET8_0_OR_GREATER
|
|
foreach (string name in Enum.GetNames<BasicDataType>())
|
|
#else
|
|
foreach (string name in Enum.GetNames(typeof(BasicDataType)))
|
|
#endif
|
|
{
|
|
if (name == dataType.SymbolicName.Name)
|
|
{
|
|
#if NET8_0_OR_GREATER
|
|
basicDataType = Enum.Parse<BasicDataType>(
|
|
dataType.SymbolicName.Name);
|
|
#else
|
|
basicDataType = (BasicDataType)Enum.Parse(
|
|
typeof(BasicDataType),
|
|
dataType.SymbolicName.Name);
|
|
#endif
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
basicDataType = default;
|
|
return false;
|
|
}
|
|
}
|
|
}
|