Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -369,7 +356,8 @@ private static List<ExpandedNodeId> browseTypeDefinitions(
var typeDefinitions = new ArrayList<ExpandedNodeId>();

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;
Expand All @@ -380,7 +368,10 @@ private static List<ExpandedNodeId> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,13 @@ private List<BrowsePathTarget> follow(NodeId nodeId, List<RelativePathElement> e
} else if (elements.size() == 1) {
List<ExpandedNodeId> 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);

Expand All @@ -144,6 +150,7 @@ private List<BrowsePathTarget> follow(NodeId nodeId, List<RelativePathElement> e
} else {
UInteger remaining = nextElements.isEmpty() ? UInteger.MAX : uint(nextElements.size());

nextXni = BrowseUtil.normalize(nextXni, server.getNamespaceTable());
return List.of(new BrowsePathTarget(nextXni, remaining));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>This is required for ExpandedNodeId values returned in {@link ReferenceDescription} and
* {@link BrowsePathTarget}. See <a
* href="https://reference.opcfoundation.org/Core/Part4/v105/docs/7.30">ReferenceDescription</a>.
*
* @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;
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}