/* ======================================================================== * 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; namespace Opc.Ua.Server.Fluent { /// /// Default implementation. Wires variable, /// method, event, and condition-refresh callbacks directly onto the /// resolved ; routes manager-level callbacks /// (history, monitored-item, lifecycle) through the parent /// 's dispatcher. /// internal class NodeBuilder : INodeBuilder { public NodeBuilder(NodeManagerBuilder parent, NodeState node) { m_parent = parent ?? throw new ArgumentNullException(nameof(parent)); Node = node ?? throw new ArgumentNullException(nameof(node)); } /// public NodeState Node { get; } /// public INodeManagerBuilder Builder => m_parent; /// public INodeBuilder As() where TState : NodeState { if (Node is not TState typed) { throw ServiceResultException.Create( StatusCodes.BadTypeMismatch, "Node '{0}' is of type {1}, which is not assignable to {2}.", Node.BrowseName, Node.GetType().Name, typeof(TState).Name); } return new NodeBuilder(m_parent, typed); } /// public INodeBuilder OnRead(NodeValueEventHandler handler) { BaseVariableState v = RequireVariable("OnRead"); ThrowIfSlotOccupied(v.OnReadValue, "OnRead"); v.OnReadValue = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnRead(NodeValueSimpleEventHandler handler) { BaseVariableState v = RequireVariable("OnSimpleRead"); ThrowIfSlotOccupied(v.OnSimpleReadValue, "OnSimpleRead"); v.OnSimpleReadValue = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnWrite(NodeValueEventHandler handler) { BaseVariableState v = RequireVariable("OnWrite"); ThrowIfSlotOccupied(v.OnWriteValue, "OnWrite"); v.OnWriteValue = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnWrite(NodeValueSimpleEventHandler handler) { BaseVariableState v = RequireVariable("OnSimpleWrite"); ThrowIfSlotOccupied(v.OnSimpleWriteValue, "OnSimpleWrite"); v.OnSimpleWriteValue = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnRead(NodeValueEventHandlerAsync handler) { BaseVariableState v = RequireVariable("OnReadAsync"); ThrowIfSlotOccupied(v.OnReadValueAsync, "OnReadAsync"); v.OnReadValueAsync = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnRead(NodeValueSimpleEventHandlerAsync handler) { BaseVariableState v = RequireVariable("OnSimpleReadAsync"); ThrowIfSlotOccupied(v.OnSimpleReadValueAsync, "OnSimpleReadAsync"); v.OnSimpleReadValueAsync = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnWrite(NodeValueWriteEventHandlerAsync handler) { BaseVariableState v = RequireVariable("OnWriteAsync"); ThrowIfSlotOccupied(v.OnWriteValueAsync, "OnWriteAsync"); v.OnWriteValueAsync = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnWrite(NodeValueSimpleWriteEventHandlerAsync handler) { BaseVariableState v = RequireVariable("OnSimpleWriteAsync"); ThrowIfSlotOccupied(v.OnSimpleWriteValueAsync, "OnSimpleWriteAsync"); v.OnSimpleWriteValueAsync = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnCall(GenericMethodCalledEventHandler2 handler) { MethodState m = RequireMethod("OnCall"); ThrowIfSlotOccupied(m.OnCallMethod2, "OnCall"); m.OnCallMethod2 = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnCall(GenericMethodCalledEventHandler2Async handler) { MethodState m = RequireMethod("OnCallAsync"); ThrowIfSlotOccupied(m.OnCallMethod2Async, "OnCallAsync"); m.OnCallMethod2Async = handler ?? throw new ArgumentNullException(nameof(handler)); return this; } /// public INodeBuilder OnNodeAdded(NodeLifecycleHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } m_parent.RegisterNodeAdded(Node, handler); return this; } /// public INodeBuilder OnNodeRemoved(NodeLifecycleHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } m_parent.RegisterNodeRemoved(Node, handler); return this; } /// public INodeBuilder OnHistoryRead(HistoryReadHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } m_parent.RegisterHistoryRead(Node, handler); return this; } /// public INodeBuilder OnHistoryUpdate(HistoryUpdateHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } m_parent.RegisterHistoryUpdate(Node, handler); return this; } /// public INodeBuilder OnConditionRefresh(ConditionRefreshHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } ThrowIfSlotOccupied(Node.OnConditionRefresh, "OnConditionRefresh"); // Direct wire — signature matches NodeStateConditionRefreshEventHandler. Node.OnConditionRefresh = (ctx, n, evts) => handler(ctx, n, evts); return this; } /// public INodeBuilder OnMonitoredItemCreated(MonitoredItemCreatedHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } m_parent.RegisterMonitoredItemCreated(Node, handler); return this; } /// public INodeBuilder OnEvent(EventNotificationHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } // OnReportEvent is also used by CustomNodeManager2 for root notifier // wiring — replacing it would silently break server event propagation. ThrowIfSlotOccupied(Node.OnReportEvent, "OnEvent (NodeState.OnReportEvent)"); Node.OnReportEvent = (ctx, n, e) => handler(ctx, n, e); return this; } /// public INodeBuilder AllowMultipleEventConsumers(bool enable = true) { m_parent.RegisterMultiConsumerNode(Node, enable); return this; } /// public INodeBuilder Child(QualifiedName browseName) { NodeState child = ResolveChild(browseName); return new NodeBuilder(m_parent, child); } /// public INodeBuilder Child(QualifiedName browseName) where TState : NodeState { NodeState child = ResolveChild(browseName); if (child is not TState typed) { throw ServiceResultException.Create( StatusCodes.BadTypeMismatch, "Child '{0}' under '{1}' is of type {2}, which is not assignable to {3}.", browseName, Node.BrowseName, child.GetType().Name, typeof(TState).Name); } return new NodeBuilder(m_parent, typed); } /// public IVariableBuilder Variable(QualifiedName browseName) { NodeState child = ResolveChild(browseName); return m_parent.ToVariableBuilder( child, CoreUtils.Format("{0}/{1}", Node.BrowseName, browseName)); } private BaseInstanceState ResolveChild(QualifiedName browseName) { if (browseName.IsNull) { throw ServiceResultException.Create( StatusCodes.BadBrowseNameInvalid, "Browse name is null or empty."); } return Node.FindChild(m_parent.Context, browseName) ?? throw ServiceResultException.Create( StatusCodes.BadNodeIdUnknown, "Child '{0}' not found under '{1}' (id '{2}').", browseName, Node.BrowseName, Node.NodeId); } private BaseVariableState RequireVariable(string what) { if (Node is not BaseVariableState v) { throw ServiceResultException.Create( StatusCodes.BadInvalidArgument, "Cannot wire {0} on node '{1}': not a BaseVariableState (actual: {2}).", what, Node.BrowseName, Node.GetType().Name); } return v; } private MethodState RequireMethod(string what) { if (Node is not MethodState m) { throw ServiceResultException.Create( StatusCodes.BadInvalidArgument, "Cannot wire {0} on node '{1}': not a MethodState (actual: {2}).", what, Node.BrowseName, Node.GetType().Name); } return m; } private void ThrowIfSlotOccupied(Delegate? existing, string what) { if (existing != null) { throw ServiceResultException.Create( StatusCodes.BadConfigurationError, "Node '{0}' (id '{1}') already has a {2} handler assigned.", Node.BrowseName, Node.NodeId, what); } } private readonly NodeManagerBuilder m_parent; } /// /// Strongly-typed wrapper. Inherits /// all On* behavior from the non-generic /// ; the only added value is a typed /// property. /// /// /// The concrete derivative the resolved node /// is narrowed to. /// internal class NodeBuilder : NodeBuilder, INodeBuilder where TState : NodeState { public NodeBuilder(NodeManagerBuilder parent, TState node) : base(parent, node) { Node = node; } /// public new TState Node { get; } } }