mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-25 11:50:03 +08:00
This change adds a **new Opc.Ua.Types** nuget that contains core types that do not rely on generated code. Why the name? Because almost all code that was in the Types subfolder is part of it, and because we do not want to change the name of the Opc.Ua.Core nuget/assembly because code relies on it. Any other name like base or bootstrap felt out of place due to the current structure already defining the name "types". Key design is to depend on only a small set of libraries with limited transitive dependencies, that includes hashcode, logging.abstraction, and for non standard20 builds System.Text.Json (needed for localizedText mul parsing). Also, key is to support .netstd 2.0 to support source generators and to allow removal of EEC_SUPPORT defines by removing std2.0 from core and all libraries. The changes are mostly restructuring, and thus small but contain some key changes: - **Status code** struct now comes with a Symbol field, which is used to carry symbols in case the code is not generated. Currently (until generated) the status code struct must be manually created with a symbol or the ToSymbolicId extension must be called in the core project. Future will generate Status code constants just like NodeId that contain the symbol which also removes the reflection-based lookup. Several tests needed to be adjusted as they checked the status code symbol strings instead of the code. - **Core identifiers** are copied as they were needed for code to compile, that includes attributes, a subset of status codes and node identifiers. These will never change so no need to generate them. - The types folder now contains **encoders**, except for json which is still in core. Once the json encoder is ported to system.text.json it will also be included and Newtonsoft.Json dropped as dependency (-> #3329). - Since types relies on **IOperationContext** and **ISystemContext**, and these export IUserIdentity which relies on crypto, these were split and extended in core with ISessionOperationContext and ISessionSystemContext, which mean changes to ServerSystemContext and introduction of SessionOperationContext and SessionSystemContext classes that extend OperationContext and SystemContext. Some runtime checks in the server are therefore needed to get at the session and auth info, however, those are anyway optional fields and are not on the critical paths, and thus a type check is in order (and was done in the past already, but against OperationContext class). - **Manifest resources** are now split between Core (nodeset zip, some generated resoures) and Types (built in bsd/xsds). This needs to be revisited later if the bsd/xsd need to be regenerated during type generation. Note that the embedded resource names changed as a result. Since these are only used internally and do not constitute public API this feels reasonable vs duplication of resources in 2 assemblies. - Reflection however now needs to load not just the built in **encodeables** from Opc.Ua.Types assembly, but also the generated encodeables from Opc.Ua.Core assembly. For this a new helper Utils method was added to get the Core assembly when linked against Opc.Ua.Core otherwise null. - Whatever utils methods needed to be split out into the new Opc.Ua.Types assembly, they are now in **CoreUtils** static class in Opc.Ua.Types, and Utils.xxxx forwards to them to ensure minimal changes. Later we would want to obsolete them in favor of CoreUtils, although it might make sense to split them again into domain specific utilities, such as networking, files, etc
1266 lines
37 KiB
C#
1266 lines
37 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua.Redaction;
|
|
|
|
namespace Opc.Ua.Client
|
|
{
|
|
/// <summary>
|
|
/// An implementation of a client side nodecache.
|
|
/// </summary>
|
|
public class NodeCache : INodeCache, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// </summary>
|
|
public NodeCache(INodeCacheContext context, ITelemetryContext telemetry)
|
|
{
|
|
m_context = context ?? throw new ArgumentNullException(nameof(context));
|
|
m_logger = telemetry.CreateLogger<NodeCache>();
|
|
m_typeTree = new TypeTable(m_context.NamespaceUris);
|
|
m_nodes = new NodeTable(
|
|
m_context.NamespaceUris,
|
|
m_context.ServerUris,
|
|
m_typeTree);
|
|
m_uaTypesLoaded = false;
|
|
m_cacheLock = new ReaderWriterLockSlim();
|
|
}
|
|
|
|
/// <summary>
|
|
/// An overrideable version of the Dispose.
|
|
/// </summary>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
m_cacheLock?.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose()
|
|
{
|
|
Dispose(disposing: true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public NamespaceTable NamespaceUris => m_context.NamespaceUris;
|
|
|
|
/// <inheritdoc/>
|
|
public StringTable ServerUris => m_context.ServerUris;
|
|
|
|
/// <inheritdoc/>
|
|
IAsyncTypeTable IAsyncNodeTable.TypeTree => this;
|
|
|
|
/// <summary>
|
|
/// Legacy type table adapter for backwards compatibility.
|
|
/// </summary>
|
|
public ITypeTable TypeTree => this.AsTypeTable();
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<INode?> FindAsync(
|
|
ExpandedNodeId nodeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
// check for null.
|
|
if (NodeId.IsNull(nodeId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
INode node;
|
|
try
|
|
{
|
|
// check if node already exists.
|
|
node = m_nodes.Find(nodeId);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
if (node != null)
|
|
{
|
|
// do not return temporary nodes created after a Browse().
|
|
if (node.GetType() != typeof(Node))
|
|
{
|
|
return node;
|
|
}
|
|
}
|
|
|
|
// fetch node from server.
|
|
try
|
|
{
|
|
return await FetchNodeAsync(nodeId, ct).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError(
|
|
"Could not fetch node from server: NodeId={NodeId}, Reason='{Error}'.",
|
|
nodeId,
|
|
Redact.Create(e));
|
|
// m_nodes[nodeId] = null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<IList<INode?>> FindAsync(
|
|
IList<ExpandedNodeId> nodeIds,
|
|
CancellationToken ct = default)
|
|
{
|
|
// check for null.
|
|
if (nodeIds == null || nodeIds.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
int count = nodeIds.Count;
|
|
var nodes = new List<INode?>(count);
|
|
var fetchNodeIds = new ExpandedNodeIdCollection();
|
|
|
|
int ii;
|
|
for (ii = 0; ii < count; ii++)
|
|
{
|
|
INode node;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
// check if node already exists.
|
|
node = m_nodes.Find(nodeIds[ii]);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
// do not return temporary nodes created after a Browse().
|
|
if (node != null && node.GetType() != typeof(Node))
|
|
{
|
|
nodes.Add(node);
|
|
}
|
|
else
|
|
{
|
|
nodes.Add(null);
|
|
fetchNodeIds.Add(nodeIds[ii]);
|
|
}
|
|
}
|
|
|
|
if (fetchNodeIds.Count == 0)
|
|
{
|
|
return nodes;
|
|
}
|
|
|
|
// fetch missing nodes from server.
|
|
IList<Node?> fetchedNodes;
|
|
try
|
|
{
|
|
fetchedNodes = await FetchNodesAsync(fetchNodeIds, ct).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError("Could not fetch nodes from server: Reason='{Error}'.", e.Message);
|
|
// m_nodes[nodeId] = null;
|
|
return nodes;
|
|
}
|
|
|
|
ii = 0;
|
|
foreach (Node? fetchedNode in fetchedNodes)
|
|
{
|
|
while (ii < count && nodes[ii] != null)
|
|
{
|
|
ii++;
|
|
}
|
|
if (ii < count && nodes[ii] == null)
|
|
{
|
|
nodes[ii++] = fetchedNode;
|
|
}
|
|
else
|
|
{
|
|
m_logger.LogError(
|
|
"Inconsistency fetching nodes from server. Not all nodes could be assigned.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<INode?> FindAsync(
|
|
ExpandedNodeId sourceId,
|
|
NodeId referenceTypeId,
|
|
bool isInverse,
|
|
bool includeSubtypes,
|
|
QualifiedName browseName,
|
|
CancellationToken ct = default)
|
|
{
|
|
// find the source.
|
|
if (await FindAsync(sourceId, ct).ConfigureAwait(false)
|
|
is not Node source)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
// find all references.
|
|
references = source.ReferenceTable
|
|
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
foreach (IReference reference in references)
|
|
{
|
|
INode? target = await FindAsync(reference.TargetId, ct)
|
|
.ConfigureAwait(false);
|
|
|
|
if (target == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (target.BrowseName == browseName)
|
|
{
|
|
return target;
|
|
}
|
|
}
|
|
|
|
// target not found.
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<NodeId> FindSuperTypeAsync(
|
|
ExpandedNodeId typeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
|
|
|
|
if (type == null)
|
|
{
|
|
return NodeId.Null;
|
|
}
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
return m_typeTree.FindSuperType(typeId);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<IList<INode>> FindAsync(
|
|
ExpandedNodeId sourceId,
|
|
NodeId referenceTypeId,
|
|
bool isInverse,
|
|
bool includeSubtypes,
|
|
CancellationToken ct = default)
|
|
{
|
|
var hits = new List<INode>();
|
|
|
|
// find the source.
|
|
if (await FindAsync(sourceId, ct).ConfigureAwait(false)
|
|
is not Node source)
|
|
{
|
|
return hits;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
// find all references.
|
|
references = source.ReferenceTable
|
|
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
foreach (IReference reference in references)
|
|
{
|
|
INode? target =
|
|
await FindAsync(reference.TargetId, ct).ConfigureAwait(false);
|
|
|
|
if (target == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
hits.Add(target);
|
|
}
|
|
|
|
return hits;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<NodeId> FindSuperTypeAsync(
|
|
NodeId typeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
|
|
|
|
if (type == null)
|
|
{
|
|
return NodeId.Null;
|
|
}
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
return m_typeTree.FindSuperType(typeId);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Node?> FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken ct)
|
|
{
|
|
var localId = ExpandedNodeId.ToNodeId(nodeId, m_context.NamespaceUris);
|
|
|
|
if (localId == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// fetch node from server.
|
|
Node source = await m_context.FetchNodeAsync(null, localId, ct: ct).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
// fetch references from server.
|
|
ReferenceDescriptionCollection references = await m_context
|
|
.FetchReferencesAsync(null, localId, ct)
|
|
.ConfigureAwait(false);
|
|
|
|
m_cacheLock.EnterUpgradeableReadLock();
|
|
try
|
|
{
|
|
foreach (ReferenceDescription reference in references)
|
|
{
|
|
// create a placeholder for the node if it does not already exist.
|
|
if (!m_nodes.Exists(reference.NodeId))
|
|
{
|
|
// transform absolute identifiers.
|
|
if (reference.NodeId != null && reference.NodeId.IsAbsolute)
|
|
{
|
|
reference.NodeId = ExpandedNodeId.ToNodeId(
|
|
reference.NodeId,
|
|
NamespaceUris);
|
|
}
|
|
|
|
var target = new Node(reference);
|
|
|
|
InternalWriteLockedAttach(target);
|
|
}
|
|
|
|
// add the reference.
|
|
source.ReferenceTable
|
|
.Add(reference.ReferenceTypeId, !reference.IsForward, reference.NodeId);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitUpgradeableReadLock();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError(
|
|
"Could not fetch references for valid node with NodeId = {NodeId}. Error = {Error}",
|
|
nodeId,
|
|
Redact.Create(e));
|
|
}
|
|
|
|
InternalWriteLockedAttach(source);
|
|
|
|
return source;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<IList<Node?>> FetchNodesAsync(
|
|
IList<ExpandedNodeId> nodeIds,
|
|
CancellationToken ct)
|
|
{
|
|
int count = nodeIds.Count;
|
|
if (count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var localIds = new NodeIdCollection(
|
|
nodeIds.Select(nodeId => ExpandedNodeId.ToNodeId(nodeId, m_context.NamespaceUris)));
|
|
|
|
// fetch nodes and references from server.
|
|
(IReadOnlyList<Node> sourceNodes, IReadOnlyList<ServiceResult> readErrors) = await m_context
|
|
.FetchNodesAsync(null, localIds, NodeClass.Unspecified, ct: ct)
|
|
.ConfigureAwait(false);
|
|
(IReadOnlyList<ReferenceDescriptionCollection> referenceCollectionList, IReadOnlyList<ServiceResult> fetchErrors) =
|
|
await m_context.FetchReferencesAsync(null, localIds, ct).ConfigureAwait(false);
|
|
|
|
int ii = 0;
|
|
for (ii = 0; ii < count; ii++)
|
|
{
|
|
if (ServiceResult.IsBad(readErrors[ii]))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ServiceResult.IsBad(fetchErrors[ii]))
|
|
{
|
|
// fetch references from server.
|
|
foreach (ReferenceDescription reference in referenceCollectionList[ii])
|
|
{
|
|
m_cacheLock.EnterUpgradeableReadLock();
|
|
try
|
|
{
|
|
// create a placeholder for the node if it does not already exist.
|
|
if (!m_nodes.Exists(reference.NodeId))
|
|
{
|
|
// transform absolute identifiers.
|
|
if (reference.NodeId != null && reference.NodeId.IsAbsolute)
|
|
{
|
|
reference.NodeId = ExpandedNodeId.ToNodeId(
|
|
reference.NodeId,
|
|
NamespaceUris);
|
|
}
|
|
|
|
var target = new Node(reference);
|
|
|
|
InternalWriteLockedAttach(target);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitUpgradeableReadLock();
|
|
}
|
|
|
|
// add the reference.
|
|
sourceNodes[ii]
|
|
.ReferenceTable.Add(
|
|
reference.ReferenceTypeId,
|
|
!reference.IsForward,
|
|
reference.NodeId);
|
|
}
|
|
}
|
|
|
|
InternalWriteLockedAttach(sourceNodes[ii]);
|
|
}
|
|
|
|
return [.. sourceNodes];
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<IList<INode>> FindReferencesAsync(
|
|
ExpandedNodeId nodeId,
|
|
NodeId referenceTypeId,
|
|
bool isInverse,
|
|
bool includeSubtypes,
|
|
CancellationToken ct)
|
|
{
|
|
IList<INode> targets = [];
|
|
|
|
if (await FindAsync(nodeId, ct).ConfigureAwait(false) is not Node source)
|
|
{
|
|
return targets;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = source.ReferenceTable
|
|
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
var targetIds = new ExpandedNodeIdCollection(
|
|
references.Select(reference => reference.TargetId));
|
|
|
|
IList<INode?> result = await FindAsync(targetIds, ct).ConfigureAwait(false);
|
|
|
|
foreach (INode? target in result)
|
|
{
|
|
if (target != null)
|
|
{
|
|
targets.Add(target);
|
|
}
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<IList<INode>> FindReferencesAsync(
|
|
IList<ExpandedNodeId> nodeIds,
|
|
IList<NodeId> referenceTypeIds,
|
|
bool isInverse,
|
|
bool includeSubtypes,
|
|
CancellationToken ct)
|
|
{
|
|
IList<INode> targets = [];
|
|
if (nodeIds.Count == 0 || referenceTypeIds.Count == 0)
|
|
{
|
|
return targets;
|
|
}
|
|
var targetIds = new ExpandedNodeIdCollection();
|
|
IList<INode?> sources = await FindAsync(nodeIds, ct).ConfigureAwait(false);
|
|
foreach (INode? source in sources)
|
|
{
|
|
if (source is not Node node)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (NodeId referenceTypeId in referenceTypeIds)
|
|
{
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = node.ReferenceTable
|
|
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
targetIds.AddRange(references.Select(reference => reference.TargetId));
|
|
}
|
|
}
|
|
|
|
IList<INode?> result = await FindAsync(targetIds, ct).ConfigureAwait(false);
|
|
foreach (INode? target in result)
|
|
{
|
|
if (target != null)
|
|
{
|
|
targets.Add(target);
|
|
}
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task FetchSuperTypesAsync(ExpandedNodeId nodeId, CancellationToken ct)
|
|
{
|
|
// find the target node,
|
|
if (await FindAsync(nodeId, ct).ConfigureAwait(false) is not ILocalNode source)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// follow the tree.
|
|
ILocalNode? subType = source;
|
|
|
|
while (subType != null)
|
|
{
|
|
ILocalNode? superType = null;
|
|
|
|
// Get super type (should be 1 or none)
|
|
IList<INode> references = await FindReferencesAsync(
|
|
subType.NodeId,
|
|
ReferenceTypeIds.HasSubtype,
|
|
true,
|
|
true,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (references != null && references.Count > 0)
|
|
{
|
|
superType = references[0] as ILocalNode;
|
|
}
|
|
|
|
subType = superType;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> ExistsAsync(
|
|
ExpandedNodeId nodeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
return await FindAsync(nodeId, ct).ConfigureAwait(false) != null;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> IsKnownAsync(
|
|
ExpandedNodeId typeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
|
|
|
|
if (type == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
return m_typeTree.IsKnown(typeId);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> IsKnownAsync(
|
|
NodeId typeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
|
|
|
|
if (type == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
return m_typeTree.IsKnown(typeId);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<IList<NodeId>> FindSubTypesAsync(
|
|
ExpandedNodeId typeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var subtypes = new List<NodeId>();
|
|
|
|
if (await FindAsync(typeId, ct).ConfigureAwait(false)
|
|
is not ILocalNode type)
|
|
{
|
|
return subtypes;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = type.References
|
|
.Find(ReferenceTypeIds.HasSubtype, false, true, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
foreach (IReference reference in references)
|
|
{
|
|
if (!reference.TargetId.IsAbsolute)
|
|
{
|
|
subtypes.Add((NodeId)reference.TargetId);
|
|
}
|
|
}
|
|
|
|
return subtypes;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> IsTypeOfAsync(
|
|
ExpandedNodeId subTypeId,
|
|
ExpandedNodeId superTypeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (subTypeId == superTypeId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (await FindAsync(subTypeId, ct).ConfigureAwait(false)
|
|
is not ILocalNode subtype)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ILocalNode? supertype = subtype;
|
|
|
|
while (supertype != null)
|
|
{
|
|
ExpandedNodeId currentId;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
currentId = supertype.References
|
|
.FindTarget(ReferenceTypeIds.HasSubtype, true, true, m_typeTree, 0);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
if (currentId == superTypeId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
supertype = await FindAsync(currentId, ct).ConfigureAwait(false) as ILocalNode;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> IsTypeOfAsync(
|
|
NodeId subTypeId,
|
|
NodeId superTypeId,
|
|
CancellationToken ct)
|
|
{
|
|
if (subTypeId == superTypeId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (await FindAsync(subTypeId, ct).ConfigureAwait(false)
|
|
is not ILocalNode subtype)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ILocalNode? supertype = subtype;
|
|
|
|
while (supertype != null)
|
|
{
|
|
ExpandedNodeId currentId;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
currentId = supertype.References
|
|
.FindTarget(ReferenceTypeIds.HasSubtype, true, true, m_typeTree, 0);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
if (currentId == superTypeId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
supertype = await FindAsync(currentId, ct).ConfigureAwait(false) as
|
|
ILocalNode;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<QualifiedName?> FindReferenceTypeNameAsync(
|
|
NodeId referenceTypeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
QualifiedName? typeName;
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
typeName = m_typeTree.FindReferenceTypeName(referenceTypeId);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
return new ValueTask<QualifiedName?>(typeName);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<NodeId> FindReferenceTypeAsync(
|
|
QualifiedName browseName,
|
|
CancellationToken ct = default)
|
|
{
|
|
NodeId referenceType;
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
referenceType = m_typeTree.FindReferenceType(browseName);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
return new ValueTask<NodeId>(referenceType);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> IsEncodingOfAsync(
|
|
ExpandedNodeId encodingId,
|
|
ExpandedNodeId datatypeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (await FindAsync(encodingId, ct).ConfigureAwait(false)
|
|
is not ILocalNode encoding)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = encoding.References
|
|
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
foreach (IReference reference in references)
|
|
{
|
|
if (reference.TargetId == datatypeId)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// no match.
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> IsEncodingForAsync(
|
|
NodeId expectedTypeId,
|
|
ExtensionObject value,
|
|
CancellationToken ct = default)
|
|
{
|
|
// no match on null values.
|
|
if (value == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// check for exact match.
|
|
if (expectedTypeId == value.TypeId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// find the encoding.
|
|
if (await FindAsync(value.TypeId, ct).ConfigureAwait(false)
|
|
is not ILocalNode encoding)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = encoding.References
|
|
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
// find data type.
|
|
foreach (IReference reference in references)
|
|
{
|
|
if (reference.TargetId == expectedTypeId)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// no match.
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> IsEncodingForAsync(
|
|
NodeId expectedTypeId,
|
|
object value,
|
|
CancellationToken ct = default)
|
|
{
|
|
// null actual datatype matches nothing.
|
|
if (value == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// null expected datatype matches everything.
|
|
if (NodeId.IsNull(expectedTypeId))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// get the actual datatype.
|
|
NodeId actualTypeId = TypeInfo.GetDataTypeId(value);
|
|
|
|
// value is valid if the expected datatype is same as or a supertype of the actual datatype
|
|
// for example: expected datatype of 'Integer' matches an actual datatype of 'UInt32'.
|
|
if (await IsTypeOfAsync(actualTypeId, expectedTypeId, ct).ConfigureAwait(false))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// allow matches non-structure values where the actual datatype
|
|
// is a supertype of the expected datatype.
|
|
// for example: expected datatype of 'UtcTime' matches an actual
|
|
// datatype of 'DateTime'.
|
|
if (actualTypeId != DataTypes.Structure)
|
|
{
|
|
return await IsTypeOfAsync(
|
|
expectedTypeId,
|
|
actualTypeId,
|
|
ct).ConfigureAwait(false);
|
|
}
|
|
|
|
// for structure types must try to determine the subtype.
|
|
|
|
if (value is ExtensionObject extension)
|
|
{
|
|
return await IsEncodingForAsync(
|
|
expectedTypeId,
|
|
extension,
|
|
ct).ConfigureAwait(false);
|
|
}
|
|
|
|
// every element in an array must match.
|
|
|
|
if (value is ExtensionObject[] extensions)
|
|
{
|
|
for (int ii = 0; ii < extensions.Length; ii++)
|
|
{
|
|
if (!await IsEncodingForAsync(
|
|
expectedTypeId,
|
|
extensions[ii],
|
|
ct).ConfigureAwait(false))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// can only get here if the value is an unrecognized data type.
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<NodeId> FindDataTypeIdAsync(
|
|
ExpandedNodeId encodingId,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (await FindAsync(encodingId, ct).ConfigureAwait(false)
|
|
is not ILocalNode encoding)
|
|
{
|
|
return NodeId.Null;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = encoding.References
|
|
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
if (references.Count > 0)
|
|
{
|
|
return ExpandedNodeId.ToNodeId(references[0].TargetId, m_context.NamespaceUris);
|
|
}
|
|
|
|
return NodeId.Null;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<NodeId> FindDataTypeIdAsync(
|
|
NodeId encodingId,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (await FindAsync(encodingId, ct).ConfigureAwait(false)
|
|
is not ILocalNode encoding)
|
|
{
|
|
return NodeId.Null;
|
|
}
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = encoding.References
|
|
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
if (references.Count > 0)
|
|
{
|
|
return ExpandedNodeId.ToNodeId(references[0].TargetId, m_context.NamespaceUris);
|
|
}
|
|
|
|
return NodeId.Null;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void LoadUaDefinedTypes(ISystemContext context)
|
|
{
|
|
if (m_uaTypesLoaded)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var predefinedNodes = new NodeStateCollection();
|
|
Assembly assembly = typeof(ReadRequest).GetTypeInfo().Assembly;
|
|
predefinedNodes.LoadFromBinaryResource(
|
|
context,
|
|
"Opc.Ua.Stack.Generated.Opc.Ua.PredefinedNodes.uanodes",
|
|
assembly,
|
|
true);
|
|
|
|
m_cacheLock.EnterWriteLock();
|
|
try
|
|
{
|
|
for (int ii = 0; ii < predefinedNodes.Count; ii++)
|
|
{
|
|
if (predefinedNodes[ii] is not BaseTypeState type)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
type.Export(context, m_nodes);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitWriteLock();
|
|
}
|
|
m_uaTypesLoaded = true;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Clear()
|
|
{
|
|
m_cacheLock.EnterWriteLock();
|
|
try
|
|
{
|
|
m_uaTypesLoaded = false;
|
|
m_nodes.Clear();
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<string?> GetDisplayTextAsync(
|
|
INode node,
|
|
CancellationToken ct = default)
|
|
{
|
|
// check for null.
|
|
if (node == null)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
// check for remote node.
|
|
if (node is not Node target)
|
|
{
|
|
return node.ToString();
|
|
}
|
|
|
|
string? displayText = null;
|
|
|
|
// use the modelling rule to determine which parent to follow.
|
|
NodeId modellingRule = target.ModellingRule;
|
|
|
|
IList<IReference> references;
|
|
|
|
m_cacheLock.EnterReadLock();
|
|
try
|
|
{
|
|
references = target.ReferenceTable
|
|
.Find(ReferenceTypeIds.Aggregates, true, true, m_typeTree);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitReadLock();
|
|
}
|
|
|
|
foreach (IReference reference in references)
|
|
{
|
|
var parent = await FindAsync(
|
|
reference.TargetId,
|
|
ct).ConfigureAwait(false) as Node;
|
|
|
|
// use the first parent if modelling rule is new.
|
|
if (modellingRule == Objects.ModellingRule_Mandatory)
|
|
{
|
|
displayText = parent == null ? null : await GetDisplayTextAsync(
|
|
parent,
|
|
ct).ConfigureAwait(false);
|
|
break;
|
|
}
|
|
|
|
// use the type node as the parent for other modelling rules.
|
|
if (parent is VariableTypeNode or ObjectTypeNode)
|
|
{
|
|
displayText = await GetDisplayTextAsync(
|
|
parent,
|
|
ct).ConfigureAwait(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// prepend the parent display name.
|
|
if (displayText != null)
|
|
{
|
|
return Utils.Format("{0}.{1}", displayText, node);
|
|
}
|
|
|
|
// simply use the node name.
|
|
return node.ToString();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<string?> GetDisplayTextAsync(
|
|
ExpandedNodeId nodeId,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (NodeId.IsNull(nodeId))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
INode? node = await FindAsync(
|
|
nodeId,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (node != null)
|
|
{
|
|
return await GetDisplayTextAsync(
|
|
node,
|
|
ct).ConfigureAwait(false);
|
|
}
|
|
|
|
return Utils.Format("{0}", nodeId);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<string?> GetDisplayTextAsync(
|
|
ReferenceDescription reference,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (reference == null || NodeId.IsNull(reference.NodeId))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
INode? node = await FindAsync(
|
|
reference.NodeId,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (node != null)
|
|
{
|
|
return await GetDisplayTextAsync(
|
|
node,
|
|
ct).ConfigureAwait(false);
|
|
}
|
|
|
|
return reference.ToString();
|
|
}
|
|
|
|
private void InternalWriteLockedAttach(ILocalNode node)
|
|
{
|
|
m_cacheLock.EnterWriteLock();
|
|
try
|
|
{
|
|
// add to cache.
|
|
m_nodes.Attach(node);
|
|
}
|
|
finally
|
|
{
|
|
m_cacheLock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
private readonly ReaderWriterLockSlim m_cacheLock = new();
|
|
private readonly ILogger m_logger;
|
|
private readonly INodeCacheContext m_context;
|
|
private readonly TypeTable m_typeTree;
|
|
private readonly NodeTable m_nodes;
|
|
private bool m_uaTypesLoaded;
|
|
}
|
|
}
|