/* ======================================================================== * 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.IO; namespace Opc.Ua.Client { /// /// Options for controlling NodeSet export behavior. /// public class NodeSetExportOptions { /// /// Whether to export value elements for variables. /// Default is false (values are only exported for Complete option). /// public bool ExportValues { get; set; } /// /// Whether to export the ParentNodeId attribute. /// Default is false (ParentNodeId is redundant as it can be inferred from references). /// public bool ExportParentNodeId { get; set; } /// /// Whether to export user context attributes (UserAccessLevel, UserExecutable, UserWriteMask, UserRolePermissions). /// Default is false (user context attributes are not exported). /// When true, UserAccessLevel is only exported if it differs from AccessLevel. /// public bool ExportUserContext { get; set; } /// /// Gets the default export options (no values, essential attributes only). /// This produces minimal file size suitable for schema definitions. /// MethodDeclarationId is always exported. User context attributes are not exported. /// public static NodeSetExportOptions Default => new() { ExportValues = false, ExportParentNodeId = false, ExportUserContext = false }; /// /// Gets complete export options with all metadata and values. /// Use this for full data export including runtime values and user context attributes. /// MethodDeclarationId is always exported. UserAccessLevel is only exported when different from AccessLevel. /// public static NodeSetExportOptions Complete => new() { ExportValues = true, ExportParentNodeId = true, ExportUserContext = true }; } /// /// Defines numerous re-useable utility functions for clients. /// public static partial class CoreClientUtils { /// /// Exports a list of nodes from a client session to a NodeSet2 XML file. /// /// The system context containing namespace information. /// The list of nodes to export. /// The output stream to write the NodeSet2 XML to. /// The version to set in the NodeSet2 XML. /// The last modified date to set in the NodeSet2 XML. /// is null. public static void ExportNodesToNodeSet2( ISystemContext context, IList nodes, Stream outputStream, string? version = null, DateTime? lastModified = null) { ExportNodesToNodeSet2( context, nodes, outputStream, NodeSetExportOptions.Default, version, lastModified); } /// /// Exports a list of nodes from a client session to a NodeSet2 XML file. /// /// The system context containing namespace information. /// The list of nodes to export. /// The output stream to write the NodeSet2 XML to. /// Options controlling the export behavior. /// The version to set in the NodeSet2 XML. /// The last modified date to set in the NodeSet2 XML. /// is null. public static void ExportNodesToNodeSet2( ISystemContext context, IList nodes, Stream outputStream, NodeSetExportOptions options, string? version = null, DateTime? lastModified = null) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (nodes == null) { throw new ArgumentNullException(nameof(nodes)); } if (outputStream == null) { throw new ArgumentNullException(nameof(outputStream)); } if (options == null) { throw new ArgumentNullException(nameof(options)); } // Convert INode instances to NodeState instances var nodeStates = new NodeStateCollection(); foreach (INode node in nodes) { NodeState? nodeState = CreateNodeState(context, node, options); if (nodeState != null) { nodeStates.Add(nodeState); } } // Use the existing export functionality nodeStates.SaveAsNodeSet2(context, outputStream, version, lastModified); } /// /// Creates a NodeState from an INode. /// /// The system context. /// The node to convert. /// Export options. /// A NodeState representing the node. private static NodeState? CreateNodeState(ISystemContext context, INode node, NodeSetExportOptions options) { if (node == null) { return null; } NodeState? nodeState; switch (node.NodeClass) { case NodeClass.Object: { var objectNode = node as IObject; var state = new BaseObjectState(null) { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, EventNotifier = objectNode?.EventNotifier ?? 0 }; if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } case NodeClass.Variable: { var variableNode = node as IVariable; var state = new BaseDataVariableState(null) { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, DataType = variableNode?.DataType ?? DataTypeIds.BaseDataType, ValueRank = variableNode?.ValueRank ?? ValueRanks.Any, AccessLevel = variableNode?.AccessLevel ?? 0, MinimumSamplingInterval = variableNode?.MinimumSamplingInterval ?? 0, Historizing = variableNode?.Historizing ?? false }; // Export Value only if requested if (options.ExportValues && variableNode != null && !variableNode.Value.IsNull) { state.Value = variableNode.Value; } // Export UserAccessLevel only if ExportUserContext is enabled AND it differs from AccessLevel if (options.ExportUserContext && variableNode != null) { byte userAccessLevel = variableNode.UserAccessLevel; byte accessLevel = variableNode.AccessLevel; if (userAccessLevel != accessLevel) { state.UserAccessLevel = userAccessLevel; } } if (variableNode != null && variableNode.ArrayDimensions.Count > 0) { state.ArrayDimensions = variableNode.ArrayDimensions; } if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } case NodeClass.Method: { var methodNode = node as IMethod; var state = new MethodState(null) { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, Executable = methodNode?.Executable ?? false }; // Export UserExecutable only if ExportUserContext is enabled if (options.ExportUserContext && methodNode != null) { state.UserExecutable = methodNode.UserExecutable; } // Always export MethodDeclarationId (important type system metadata) if (!node.TypeDefinitionId.IsNull) { state.MethodDeclarationId = ExpandedNodeId.ToNodeId(node.TypeDefinitionId, context.NamespaceUris); } if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } case NodeClass.ObjectType: { var objectTypeNode = node as IObjectType; var state = new BaseObjectTypeState { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, IsAbstract = objectTypeNode?.IsAbstract ?? false }; if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } case NodeClass.VariableType: { var variableTypeNode = node as IVariableType; var state = new BaseDataVariableTypeState { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, IsAbstract = variableTypeNode?.IsAbstract ?? false, DataType = variableTypeNode?.DataType ?? DataTypeIds.BaseDataType, ValueRank = variableTypeNode?.ValueRank ?? ValueRanks.Any }; // Export Value only if requested if (options.ExportValues && variableTypeNode != null && !variableTypeNode.Value.IsNull) { state.Value = variableTypeNode.Value; } if (variableTypeNode?.ArrayDimensions != null && variableTypeNode.ArrayDimensions.Count > 0) { state.ArrayDimensions = variableTypeNode.ArrayDimensions; } if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } case NodeClass.DataType: { var dataTypeNode = node as IDataType; var state = new DataTypeState { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, IsAbstract = dataTypeNode?.IsAbstract ?? false }; if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } case NodeClass.ReferenceType: { var referenceTypeNode = node as IReferenceType; var state = new ReferenceTypeState { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, IsAbstract = referenceTypeNode?.IsAbstract ?? false, Symmetric = referenceTypeNode?.Symmetric ?? false, InverseName = referenceTypeNode?.InverseName ?? default }; if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } case NodeClass.View: { var viewNode = node as IView; var state = new ViewState { NodeId = ExpandedNodeId.ToNodeId(node.NodeId, context.NamespaceUris), BrowseName = node.BrowseName, DisplayName = node.DisplayName, EventNotifier = viewNode?.EventNotifier ?? 0, ContainsNoLoops = viewNode?.ContainsNoLoops ?? false }; if (node is ILocalNode localNode) { state.Description = localNode.Description; state.WriteMask = localNode.WriteMask; // Export UserWriteMask only if ExportUserContext is enabled if (options.ExportUserContext) { state.UserWriteMask = localNode.UserWriteMask; } } nodeState = state; break; } default: return null; } // Handle references - nodeState is guaranteed to be non-null here if (node is ILocalNode localNodeWithRefs) { IReferenceCollection references = localNodeWithRefs.References; if (references != null && references.Count > 0) { foreach (IReference reference in references) { nodeState.AddReference( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId); } } } return nodeState; } } }