/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua.Redaction; namespace Opc.Ua.Client { /// /// An implementation of a client side nodecache. /// public class NodeCache : INodeCache, IDisposable { /// /// Initializes the object with default values. /// public NodeCache(INodeCacheContext context, ITelemetryContext telemetry) { m_context = context ?? throw new ArgumentNullException(nameof(context)); m_logger = telemetry.CreateLogger(); 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(); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { m_cacheLock?.Dispose(); } } /// public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } /// public NamespaceTable NamespaceUris => m_context.NamespaceUris; /// public StringTable ServerUris => m_context.ServerUris; /// IAsyncTypeTable IAsyncNodeTable.TypeTree => this; /// /// Legacy type table adapter for backwards compatibility. /// public ITypeTable TypeTree => this.AsTypeTable(); /// public async ValueTask 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; } } /// public async Task> FindAsync( IList nodeIds, CancellationToken ct = default) { // check for null. if (nodeIds == null || nodeIds.Count == 0) { return []; } int count = nodeIds.Count; var nodes = new List(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 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; } /// public async ValueTask 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 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; } /// public async ValueTask 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(); } } /// public async ValueTask> FindAsync( ExpandedNodeId sourceId, NodeId referenceTypeId, bool isInverse, bool includeSubtypes, CancellationToken ct = default) { var hits = new List(); // find the source. if (await FindAsync(sourceId, ct).ConfigureAwait(false) is not Node source) { return hits; } IList 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; } /// public async ValueTask 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(); } } /// public async Task 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; } /// public async Task> FetchNodesAsync( IList 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 sourceNodes, IReadOnlyList readErrors) = await m_context .FetchNodesAsync(null, localIds, NodeClass.Unspecified, ct: ct) .ConfigureAwait(false); (IReadOnlyList referenceCollectionList, IReadOnlyList 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]; } /// public async Task> FindReferencesAsync( ExpandedNodeId nodeId, NodeId referenceTypeId, bool isInverse, bool includeSubtypes, CancellationToken ct) { IList targets = []; if (await FindAsync(nodeId, ct).ConfigureAwait(false) is not Node source) { return targets; } IList 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 result = await FindAsync(targetIds, ct).ConfigureAwait(false); foreach (INode? target in result) { if (target != null) { targets.Add(target); } } return targets; } /// public async Task> FindReferencesAsync( IList nodeIds, IList referenceTypeIds, bool isInverse, bool includeSubtypes, CancellationToken ct) { IList targets = []; if (nodeIds.Count == 0 || referenceTypeIds.Count == 0) { return targets; } var targetIds = new ExpandedNodeIdCollection(); IList 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 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 result = await FindAsync(targetIds, ct).ConfigureAwait(false); foreach (INode? target in result) { if (target != null) { targets.Add(target); } } return targets; } /// 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 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; } } /// public async ValueTask ExistsAsync( ExpandedNodeId nodeId, CancellationToken ct = default) { return await FindAsync(nodeId, ct).ConfigureAwait(false) != null; } /// public async ValueTask 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(); } } /// public async ValueTask 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(); } } /// public async ValueTask> FindSubTypesAsync( ExpandedNodeId typeId, CancellationToken ct = default) { var subtypes = new List(); if (await FindAsync(typeId, ct).ConfigureAwait(false) is not ILocalNode type) { return subtypes; } IList 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; } /// public async ValueTask 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; } /// public async ValueTask 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; } /// public ValueTask FindReferenceTypeNameAsync( NodeId referenceTypeId, CancellationToken ct = default) { QualifiedName? typeName; m_cacheLock.EnterReadLock(); try { typeName = m_typeTree.FindReferenceTypeName(referenceTypeId); } finally { m_cacheLock.ExitReadLock(); } return new ValueTask(typeName); } /// public ValueTask FindReferenceTypeAsync( QualifiedName browseName, CancellationToken ct = default) { NodeId referenceType; m_cacheLock.EnterReadLock(); try { referenceType = m_typeTree.FindReferenceType(browseName); } finally { m_cacheLock.ExitReadLock(); } return new ValueTask(referenceType); } /// public async ValueTask IsEncodingOfAsync( ExpandedNodeId encodingId, ExpandedNodeId datatypeId, CancellationToken ct = default) { if (await FindAsync(encodingId, ct).ConfigureAwait(false) is not ILocalNode encoding) { return false; } IList 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; } /// public async ValueTask 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 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; } /// public async ValueTask 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; } /// public async ValueTask FindDataTypeIdAsync( ExpandedNodeId encodingId, CancellationToken ct = default) { if (await FindAsync(encodingId, ct).ConfigureAwait(false) is not ILocalNode encoding) { return NodeId.Null; } IList 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; } /// public async ValueTask FindDataTypeIdAsync( NodeId encodingId, CancellationToken ct = default) { if (await FindAsync(encodingId, ct).ConfigureAwait(false) is not ILocalNode encoding) { return NodeId.Null; } IList 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; } /// 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; } /// public void Clear() { m_cacheLock.EnterWriteLock(); try { m_uaTypesLoaded = false; m_nodes.Clear(); } finally { m_cacheLock.ExitWriteLock(); } } /// public async ValueTask 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 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(); } /// public async ValueTask 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); } /// public async ValueTask 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; } }