diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseHelper.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseHelper.java index 59a24bb769..5662e9b60e 100644 --- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseHelper.java +++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseHelper.java @@ -260,20 +260,7 @@ private static ReferenceDescription createReferenceDescription( boolean forward = masks.contains(BrowseResultMask.IsForward) && reference.isForward(); ExpandedNodeId targetNodeId = reference.getTargetNodeId(); - - // From https://reference.opcfoundation.org/Core/Part4/v105/docs/7.30: - // If the server index indicates that the TargetNode is a remote Node, then the nodeId shall - // contain the absolute namespace URI. If the TargetNode is a local Node, the nodeId shall - // contain the namespace index. - if (targetNodeId.isLocal()) { - if (targetNodeId.isAbsolute()) { - targetNodeId = targetNodeId.relative(namespaceTable).orElseThrow(); - } - } else { - if (targetNodeId.isRelative()) { - targetNodeId = targetNodeId.absolute(namespaceTable).orElseThrow(); - } - } + targetNodeId = BrowseUtil.normalize(targetNodeId, namespaceTable); return new ReferenceDescription( referenceTypeId, @@ -369,7 +356,8 @@ private static List browseTypeDefinitions( var typeDefinitions = new ArrayList(); for (int i = 0; i < nodeIds.size(); i++) { - NodeId nodeId = nodeIds.get(i).toNodeId(server.getNamespaceTable()).orElse(NodeId.NULL_VALUE); + NamespaceTable namespaceTable = server.getNamespaceTable(); + NodeId nodeId = nodeIds.get(i).toNodeId(namespaceTable).orElse(NodeId.NULL_VALUE); BrowseAttributes attributes = browseAttributes.get(i); NodeClass nodeClass = attributes.nodeClass; @@ -380,7 +368,10 @@ private static List browseTypeDefinitions( switch (attributes.nodeClass) { case Object, Variable -> { try { - typeDefinitions.add(browseTypeDefinition(server, nodeId)); + ExpandedNodeId typeDefinitionId = browseTypeDefinition(server, nodeId); + typeDefinitionId = BrowseUtil.normalize(typeDefinitionId, namespaceTable); + + typeDefinitions.add(typeDefinitionId); } catch (UaException e) { LoggerFactory.getLogger(BrowseHelper.class) .error("Error browsing TypeDefinition for nodeId={}", nodeId, e); diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowsePathsHelper.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowsePathsHelper.java index 6cf5a09d59..b50f1ea037 100644 --- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowsePathsHelper.java +++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowsePathsHelper.java @@ -125,7 +125,13 @@ private List follow(NodeId nodeId, List e } else if (elements.size() == 1) { List targets = target(nodeId, elements.get(0)); - return targets.stream().map(n -> new BrowsePathTarget(n, UInteger.MAX)).collect(toList()); + return targets.stream() + .map( + n -> { + n = BrowseUtil.normalize(n, server.getNamespaceTable()); + return new BrowsePathTarget(n, UInteger.MAX); + }) + .collect(toList()); } else { RelativePathElement e = elements.get(0); @@ -144,6 +150,7 @@ private List follow(NodeId nodeId, List e } else { UInteger remaining = nextElements.isEmpty() ? UInteger.MAX : uint(nextElements.size()); + nextXni = BrowseUtil.normalize(nextXni, server.getNamespaceTable()); return List.of(new BrowsePathTarget(nextXni, remaining)); } } diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseUtil.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseUtil.java new file mode 100644 index 0000000000..36f8cbbb6e --- /dev/null +++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 the Eclipse Milo Authors + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.milo.opcua.sdk.server.servicesets.impl.helpers; + +import org.eclipse.milo.opcua.stack.core.NamespaceTable; +import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId; +import org.eclipse.milo.opcua.stack.core.types.structured.BrowsePathTarget; +import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription; + +class BrowseUtil { + + private BrowseUtil() {} + + /** + * "Normalize" an {@link ExpandedNodeId} to be relative (index-based) if it is local and absolute + * (uri-based) if it is not. + * + *

This is required for ExpandedNodeId values returned in {@link ReferenceDescription} and + * {@link BrowsePathTarget}. See ReferenceDescription. + * + * @param nodeId the ExpandedNodeId to normalize. + * @param namespaceTable the NamespaceTable to use. + * @return the normalized ExpandedNodeId. + */ + public static ExpandedNodeId normalize(ExpandedNodeId nodeId, NamespaceTable namespaceTable) { + if (nodeId.isLocal()) { + if (nodeId.isAbsolute()) { + nodeId = nodeId.relative(namespaceTable).orElseThrow(); + } + } else { + if (nodeId.isRelative()) { + nodeId = nodeId.absolute(namespaceTable).orElseThrow(); + } + } + return nodeId; + } +} diff --git a/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseUtilTest.java b/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseUtilTest.java new file mode 100644 index 0000000000..dbfdfc2318 --- /dev/null +++ b/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/servicesets/impl/helpers/BrowseUtilTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025 the Eclipse Milo Authors + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.milo.opcua.sdk.server.servicesets.impl.helpers; + +import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort; +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.milo.opcua.stack.core.NamespaceTable; +import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId; +import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId.NamespaceReference; +import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId.ServerReference; +import org.junit.jupiter.api.Test; + +class BrowseUtilTest { + + @Test + void testNormalizeLocalAbsolute() { + NamespaceTable namespaceTable = new NamespaceTable(); + namespaceTable.add("uri:test"); + + // Local (server index 0) and absolute (namespace URI) + ExpandedNodeId nodeId = ExpandedNodeId.of("uri:test", "test"); + assertTrue(nodeId.isLocal()); + assertTrue(nodeId.isAbsolute()); + + // Normalize should convert to relative + ExpandedNodeId normalized = BrowseUtil.normalize(nodeId, namespaceTable); + assertTrue(normalized.isLocal()); + assertTrue(normalized.isRelative()); + assertEquals(ushort(1), normalized.getNamespaceIndex()); + assertEquals("test", normalized.getIdentifier()); + } + + @Test + void testNormalizeLocalRelative() { + NamespaceTable namespaceTable = new NamespaceTable(); + namespaceTable.add("uri:test"); + + // Local (server index 0) and relative (namespace index) + ExpandedNodeId nodeId = ExpandedNodeId.of(ushort(1), "test"); + assertTrue(nodeId.isLocal()); + assertTrue(nodeId.isRelative()); + + // Normalize should keep it unchanged + ExpandedNodeId normalized = BrowseUtil.normalize(nodeId, namespaceTable); + assertTrue(normalized.isLocal()); + assertTrue(normalized.isRelative()); + assertEquals(ushort(1), normalized.getNamespaceIndex()); + assertEquals("test", normalized.getIdentifier()); + assertSame(nodeId, normalized); // Should be the same instance + } + + @Test + void testNormalizeNonLocalRelative() { + NamespaceTable namespaceTable = new NamespaceTable(); + namespaceTable.add("uri:test"); + + // Non-local (server index 1) and relative (namespace index) + ExpandedNodeId nodeId = + new ExpandedNodeId(ServerReference.of(1), NamespaceReference.of(ushort(1)), "test"); + assertFalse(nodeId.isLocal()); + assertTrue(nodeId.isRelative()); + + // Normalize should convert to absolute + ExpandedNodeId normalized = BrowseUtil.normalize(nodeId, namespaceTable); + assertFalse(normalized.isLocal()); + assertTrue(normalized.isAbsolute()); + assertEquals("uri:test", normalized.getNamespaceUri()); + assertEquals("test", normalized.getIdentifier()); + } + + @Test + void testNormalizeNonLocalAbsolute() { + NamespaceTable namespaceTable = new NamespaceTable(); + namespaceTable.add("uri:test"); + + // Non-local (server index 1) and absolute (namespace URI) + ExpandedNodeId nodeId = + new ExpandedNodeId(ServerReference.of(1), NamespaceReference.of("uri:test"), "test"); + assertFalse(nodeId.isLocal()); + assertTrue(nodeId.isAbsolute()); + + // Normalize should keep it unchanged + ExpandedNodeId normalized = BrowseUtil.normalize(nodeId, namespaceTable); + assertFalse(normalized.isLocal()); + assertTrue(normalized.isAbsolute()); + assertEquals("uri:test", normalized.getNamespaceUri()); + assertEquals("test", normalized.getIdentifier()); + assertSame(nodeId, normalized); // Should be the same instance + } +}