/* ========================================================================
* 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
{
///
/// Extension methods that add references and child objects to the
/// node currently focused by the fluent builder. These cover the
/// common "FunctionalGroup" wiring pattern from OPC UA DI
/// (OPC 10000-100):
///
///
///
/// -
/// — adds an Organizes reference (DI uses this to gather
/// unrelated variables under a FunctionalGroup).
/// -
/// /
/// — common hierarchical references.
/// -
/// — generic escape hatch for any
/// reference type.
/// -
/// — creates a new child (typically
/// of Opc.Ua.Di.ObjectTypes.FunctionalGroupType) for grouping
/// other nodes.
///
///
/// Newly created child objects are attached to the parent via
/// ; their NodeIds are generated from
/// the parent's NodeId by appending the child browse name (matching
/// the pattern used by the generated state classes and the
/// PumpDeviceIntegrationServer NodeIdFactory). Direct NodeId lookup on a newly
/// created child requires the owning node manager to index the new
/// node via AddPredefinedNodeAsync; until then the child is
/// reachable through navigation from the parent.
///
///
public static class ReferenceBuilderExtensions
{
///
/// Adds an reference
/// from the current node to .
///
/// The owning node builder.
/// NodeId of the organized target.
/// The same builder, for chaining.
public static INodeBuilder Organizes(
this INodeBuilder builder,
NodeId targetId)
{
return builder.AddReference(ReferenceTypeIds.Organizes, isInverse: false, targetId);
}
///
/// Convenience overload that takes a for
/// the target.
///
public static INodeBuilder Organizes(
this INodeBuilder builder,
NodeState target)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}
return builder.Organizes(target.NodeId);
}
///
/// Adds a reference
/// from the current node to .
///
public static INodeBuilder HasComponent(
this INodeBuilder builder,
NodeId targetId)
{
return builder.AddReference(ReferenceTypeIds.HasComponent, isInverse: false, targetId);
}
///
/// Adds a reference
/// from the current node to .
///
public static INodeBuilder HasProperty(
this INodeBuilder builder,
NodeId targetId)
{
return builder.AddReference(ReferenceTypeIds.HasProperty, isInverse: false, targetId);
}
///
/// Adds an arbitrary reference from the current node to
/// . Equivalent to
/// .
///
/// The owning node builder.
/// The reference type id.
///
/// for an inverse reference;
/// for a forward reference.
///
/// NodeId of the target node.
/// The same builder, for chaining.
public static INodeBuilder AddReference(
this INodeBuilder builder,
NodeId referenceTypeId,
bool isInverse,
NodeId targetId)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (referenceTypeId.IsNull)
{
throw new ArgumentNullException(nameof(referenceTypeId));
}
if (targetId.IsNull)
{
throw new ArgumentNullException(nameof(targetId));
}
builder.Node.AddReference(referenceTypeId, isInverse, targetId);
return builder;
}
///
/// Creates and attaches a new child
/// to the resolved parent. Useful for synthesising
/// FunctionalGroups (DI) or other grouping objects in code rather
/// than declaring them in the NodeSet XML.
///
/// The owning parent builder.
///
/// Browse name of the new child (qualified with the desired
/// namespace index).
///
///
/// Optional TypeDefinitionId for the new object. When
/// (the default), the
/// is used. Pass
/// Opc.Ua.Di.ObjectTypes.FunctionalGroupType (resolved
/// through the NamespaceTable) when the group should appear as
/// a DI FunctionalGroup.
///
///
/// A typed for the
/// newly created child, ready for further wiring (e.g.
/// ).
///
public static INodeBuilder AddObject(
this INodeBuilder parent,
QualifiedName browseName,
NodeId typeDefinitionId = default)
{
if (parent == null)
{
throw new ArgumentNullException(nameof(parent));
}
if (browseName.IsNull)
{
throw new ArgumentNullException(nameof(browseName));
}
NodeId typeDef = typeDefinitionId.IsNull ? ObjectTypeIds.BaseObjectType : typeDefinitionId;
string symbolicName = browseName.Name ?? string.Empty;
var child = new BaseObjectState(parent.Node)
{
BrowseName = browseName,
DisplayName = new LocalizedText(symbolicName),
SymbolicName = symbolicName,
TypeDefinitionId = typeDef
};
// Generate a NodeId that mirrors the parent's identifier scope.
// Pattern matches PumpDeviceIntegrationServer's NodeIdFactory:
// "{parentIdentifier}_{childBrowseName}" in the parent's namespace.
string parentIdentifier = parent.Node.NodeId.IdentifierAsString;
child.NodeId = new NodeId(
string.Concat(parentIdentifier, "_", symbolicName),
parent.Node.NodeId.NamespaceIndex);
parent.Node.AddChild(child);
// Return a typed builder pointing at the new child. Reuse the
// existing NodeManagerBuilder.AsTyped path by going through a
// small adapter that doesn't require re-registration.
return new AdHocNodeBuilder(parent.Builder, child);
}
///
/// Minimal wrapper for nodes
/// that don't live in the manager's PredefinedNodes
/// dictionary. Provides direct access to the underlying node and
/// the parent builder, plus pass-through of all On* /
/// child wiring through the same mechanisms used by the
/// production NodeBuilder.
///
/// Concrete node state class.
private sealed class AdHocNodeBuilder :
INodeBuilder
where TState : NodeState
{
internal AdHocNodeBuilder(INodeManagerBuilder ownerBuilder, TState node)
{
Builder = ownerBuilder ?? throw new ArgumentNullException(nameof(ownerBuilder));
Node = node ?? throw new ArgumentNullException(nameof(node));
}
public TState Node { get; }
NodeState INodeBuilder.Node => Node;
NodeState INodeStateBuilder.Node => Node;
public INodeManagerBuilder Builder { get; }
INodeStateBuilder INodeStateBuilder.Child(QualifiedName browseName)
{
return Child(browseName);
}
INodeStateBuilder INodeStateBuilder.Child(QualifiedName browseName)
{
return Child(browseName);
}
public INodeBuilder As() where TOther : NodeState
{
if (Node is not TOther typed)
{
throw ServiceResultException.Create(
StatusCodes.BadTypeMismatch,
"Node '{0}' is not assignable to '{1}'.",
Node.BrowseName,
typeof(TOther).Name);
}
return new AdHocNodeBuilder(Builder, typed);
}
public INodeBuilder OnRead(NodeValueEventHandler handler)
{
return SetVariable(v => v.OnReadValue = handler);
}
public INodeBuilder OnRead(NodeValueSimpleEventHandler handler)
{
return SetVariable(v => v.OnSimpleReadValue = handler);
}
public INodeBuilder OnWrite(NodeValueEventHandler handler)
{
return SetVariable(v => v.OnWriteValue = handler);
}
public INodeBuilder OnWrite(NodeValueSimpleEventHandler handler)
{
return SetVariable(v => v.OnSimpleWriteValue = handler);
}
public INodeBuilder OnRead(NodeValueEventHandlerAsync handler)
{
return SetVariable(v => v.OnReadValueAsync = handler);
}
public INodeBuilder OnRead(NodeValueSimpleEventHandlerAsync handler)
{
return SetVariable(v => v.OnSimpleReadValueAsync = handler);
}
public INodeBuilder OnWrite(NodeValueWriteEventHandlerAsync handler)
{
return SetVariable(v => v.OnWriteValueAsync = handler);
}
public INodeBuilder OnWrite(NodeValueSimpleWriteEventHandlerAsync handler)
{
return SetVariable(v => v.OnSimpleWriteValueAsync = handler);
}
public INodeBuilder OnCall(GenericMethodCalledEventHandler2 handler)
{
return SetMethod(m => m.OnCallMethod2 = handler);
}
public INodeBuilder OnCall(GenericMethodCalledEventHandler2Async handler)
{
return SetMethod(m => m.OnCallMethod2Async = handler);
}
public INodeBuilder OnNodeAdded(NodeLifecycleHandler handler)
{
handler(Builder.Context, Node);
return this;
}
public INodeBuilder OnNodeRemoved(NodeLifecycleHandler handler)
{
return this;
}
public INodeBuilder OnHistoryRead(HistoryReadHandler handler)
{
return this;
}
public INodeBuilder OnHistoryUpdate(HistoryUpdateHandler handler)
{
return this;
}
public INodeBuilder OnConditionRefresh(ConditionRefreshHandler handler)
{
return this;
}
public INodeBuilder OnMonitoredItemCreated(MonitoredItemCreatedHandler handler)
{
return this;
}
public INodeBuilder OnEvent(EventNotificationHandler handler)
{
Node.OnReportEvent = (ctx, n, ev) => handler(ctx, n, ev);
return this;
}
public INodeBuilder AllowMultipleEventConsumers(bool enable = true)
{
if (Builder is NodeManagerBuilder nmb)
{
nmb.RegisterMultiConsumerNode(Node, enable);
}
return this;
}
public INodeBuilder Child(QualifiedName browseName)
{
NodeState? c = Node.FindChild(Builder.Context, browseName) ?? throw ServiceResultException.Create(
StatusCodes.BadNodeIdUnknown,
"Child '{0}' not found on '{1}'.",
browseName,
Node.BrowseName);
return new AdHocNodeBuilder(Builder, c);
}
public INodeBuilder Child(QualifiedName browseName)
where TChild : NodeState
{
NodeState? c = Node.FindChild(Builder.Context, browseName);
if (c is not TChild typed)
{
throw ServiceResultException.Create(
StatusCodes.BadTypeMismatch,
"Child '{0}' on '{1}' is not '{2}'.",
browseName, Node.BrowseName, typeof(TChild).Name);
}
return new AdHocNodeBuilder(Builder, typed);
}
public IVariableBuilder Variable(QualifiedName browseName)
{
NodeState? c = Node.FindChild(Builder.Context, browseName);
if (c is not BaseVariableState variable)
{
throw ServiceResultException.Create(
StatusCodes.BadTypeMismatch,
"Child '{0}' on '{1}' is not a variable.",
browseName, Node.BrowseName);
}
return new VariableBuilder(Builder, variable);
}
private AdHocNodeBuilder SetVariable(Action wire)
{
if (Node is not BaseVariableState v)
{
throw ServiceResultException.Create(
StatusCodes.BadInvalidArgument,
"Cannot wire variable callback on '{0}' (not a BaseVariableState).",
Node.BrowseName);
}
wire(v);
return this;
}
private AdHocNodeBuilder SetMethod(Action wire)
{
if (Node is not MethodState m)
{
throw ServiceResultException.Create(
StatusCodes.BadInvalidArgument,
"Cannot wire method callback on '{0}' (not a MethodState).",
Node.BrowseName);
}
wire(m);
return this;
}
}
}
}