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
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2026 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.examples.client;

import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElse;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.AliasNameDataType;
import org.eclipse.milo.opcua.stack.core.types.structured.AliasNameVerboseDataType;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.CallMethodResult;
import org.eclipse.milo.opcua.stack.core.types.structured.CallResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Looks up OPC UA Part 17 Alias Names on the example server: calls {@code FindAlias} and {@code
* FindAliasVerbose} on the standard {@code Aliases} Object with the pattern {@code "Demo.%"},
* decodes the results, then reads the Value of a resolved target to prove end-to-end resolution.
*
* <p>The demo aliases are created by the ExampleServer's AliasManager in a "MiloDemo" category
* organized under the standard {@code TagVariables} Object; because the {@code FindAlias} search is
* recursive, they are found from the {@code Aliases} root.
*/
public class AliasNamesExample implements ClientExample {

public static void main(String[] args) throws Exception {
AliasNamesExample example = new AliasNamesExample();

new ClientExampleRunner(example).run();
}

private final Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void run(OpcUaClient client, CompletableFuture<OpcUaClient> future) throws Exception {
client.connect();

// FindAlias is a standard Method instance defined by the base NodeSet; the pattern argument
// uses the Part 4 "Like" grammar, so "Demo.%" matches every alias name starting "Demo.".
AliasNameDataType[] aliases = findAlias(client, "Demo.%");

for (AliasNameDataType alias : aliases) {
logger.info(
"FindAlias: {} -> {}",
alias.getAliasName().name(),
Arrays.toString(alias.getReferencedNodes()));
}

if (aliases.length == 0) {
throw new UaException(StatusCodes.Bad_NotFound, "no aliases matched \"Demo.%\"");
}

// Prove end-to-end resolution: take the first alias's first target (targets are returned in
// order of preference) and read its Value attribute.
readFirstTarget(client, aliases[0]);

// FindAliasVerbose is an Optional Method the standard NodeSet does not define an instance of;
// the example server enables it on its AliasManager, which materializes the Method Node with
// a NodeId allocated in the example namespace.
NodeId findAliasVerboseId = NodeId.parse("ns=2;s=Aliases/FindAliasVerbose");

AliasNameVerboseDataType[] verboseAliases =
findAliasVerbose(client, findAliasVerboseId, "Demo.%");

for (AliasNameVerboseDataType alias : verboseAliases) {
logger.info(
"FindAliasVerbose: {} -> {} (category={})",
alias.getAliasName().name(),
Arrays.toString(alias.getReferencedNodes()),
alias.getAliasNameCategoryId());
}

future.complete(client);
}

private AliasNameDataType[] findAlias(OpcUaClient client, String pattern) throws UaException {
ExtensionObject[] xos = callFindMethod(client, NodeIds.Aliases_FindAlias, pattern);

var aliases = new AliasNameDataType[xos.length];
for (int i = 0; i < xos.length; i++) {
aliases[i] = (AliasNameDataType) xos[i].decode(client.getStaticEncodingContext());
}
return aliases;
}

private AliasNameVerboseDataType[] findAliasVerbose(
OpcUaClient client, NodeId methodId, String pattern) throws UaException {

ExtensionObject[] xos = callFindMethod(client, methodId, pattern);

var aliases = new AliasNameVerboseDataType[xos.length];
for (int i = 0; i < xos.length; i++) {
aliases[i] = (AliasNameVerboseDataType) xos[i].decode(client.getStaticEncodingContext());
}
return aliases;
}

/**
* Call a FindAlias-shaped Method on the standard {@code Aliases} Object and return the encoded
* result entries; both FindAlias and FindAliasVerbose take an alias name search pattern and a
* ReferenceType filter (a null NodeId means no restriction) and return a single output array.
*/
private ExtensionObject[] callFindMethod(OpcUaClient client, NodeId methodId, String pattern)
throws UaException {

var request =
new CallMethodRequest(
NodeIds.Aliases,
methodId,
new Variant[] {new Variant(pattern), new Variant(NodeId.NULL_VALUE)});

CallResponse response = client.call(List.of(request));

CallMethodResult result = requireNonNull(response.getResults())[0];

if (!result.getStatusCode().isGood()) {
throw new UaException(result.getStatusCode());
}

Variant[] outputs = requireNonNull(result.getOutputArguments());

return requireNonNullElse((ExtensionObject[]) outputs[0].value(), new ExtensionObject[0]);
}

private void readFirstTarget(OpcUaClient client, AliasNameDataType alias) throws UaException {
ExpandedNodeId firstTarget = requireNonNull(alias.getReferencedNodes())[0];

NodeId targetNodeId =
firstTarget
.toNodeId(client.getNamespaceTable())
.orElseThrow(
() ->
new UaException(
StatusCodes.Bad_NodeIdUnknown, "target is not local: " + firstTarget));

DataValue value = client.readValue(0.0, TimestampsToReturn.Both, targetNodeId);

logger.info(
"read {} (target {}): {}",
alias.getAliasName().name(),
targetNodeId.toParseableString(),
value.getValue().value());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
Expand All @@ -34,12 +35,18 @@
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig;
import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfigBuilder;
import org.eclipse.milo.opcua.sdk.server.aliases.AliasCategoryConfig;
import org.eclipse.milo.opcua.sdk.server.aliases.AliasManager;
import org.eclipse.milo.opcua.sdk.server.aliases.AliasManagerConfig;
import org.eclipse.milo.opcua.sdk.server.aliases.AliasTarget;
import org.eclipse.milo.opcua.sdk.server.identity.AnonymousIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.CompositeValidator;
import org.eclipse.milo.opcua.sdk.server.identity.UsernameIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.X509IdentityValidator;
import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.UaRuntimeException;
import org.eclipse.milo.opcua.stack.core.security.AbstractCertificateFactory;
import org.eclipse.milo.opcua.stack.core.security.DefaultApplicationGroup;
Expand All @@ -52,6 +59,8 @@
import org.eclipse.milo.opcua.stack.core.transport.TransportProfile;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo;
import org.eclipse.milo.opcua.stack.core.util.CertificateUtil;
Expand Down Expand Up @@ -93,6 +102,7 @@ public static void main(String[] args) throws Exception {
private final OpcUaServer server;
private final ExampleNamespace exampleNamespace;
private final AlarmConditionsNamespace alarmConditionsNamespace;
private final AliasManager aliasManager;

public ExampleServer() throws Exception {
this(DEFAULT_TCP_BIND_PORT, builder -> {});
Expand Down Expand Up @@ -242,6 +252,53 @@ protected X509Certificate[] createRsaSha256CertificateChain(KeyPair keyPair) {

alarmConditionsNamespace = new AlarmConditionsNamespace(server);
alarmConditionsNamespace.startup();

// Opt-in OPC UA Part 17 Alias Names support: binds FindAlias on the standard Aliases,
// TagVariables, and Topics Objects and, with FindAliasVerbose enabled, materializes
// FindAliasVerbose Method instances alongside them, with NodeIds allocated in the example
// namespace. The manager is started in startup(), after the server itself has started.
aliasManager =
new AliasManager(
server,
AliasManagerConfig.builder()
.nodeNamespaceIndex(exampleNamespace.getNamespaceIndex())
.findAliasVerboseEnabled(true)
.build());
}

/**
* Creates a demo alias category, "MiloDemo", organized under the standard {@code TagVariables}
* Object, with aliases targeting HelloWorld scalar Variables. The AliasNamesExample client
* example resolves these aliases via FindAlias/FindAliasVerbose and reads the targets.
*/
private void addDemoAliases() throws UaException {
var categoryConfig =
new AliasCategoryConfig(
new NodeId(exampleNamespace.getNamespaceIndex(), "Aliases/MiloDemo"),
NodeIds.TagVariables,
new QualifiedName(exampleNamespace.getNamespaceIndex().intValue(), "MiloDemo"),
exampleNamespace.getNodeManager(),
name -> new NodeId(exampleNamespace.getNamespaceIndex(), "Aliases/" + name),
false,
false,
false);

NodeId categoryId = aliasManager.addCategory(categoryConfig).nodeId();

addDemoAlias(categoryId, "Demo.ScalarDouble", "HelloWorld/ScalarTypes/Double");
addDemoAlias(categoryId, "Demo.ScalarInt32", "HelloWorld/ScalarTypes/Int32");
}

private void addDemoAlias(NodeId categoryId, String aliasName, String targetIdentifier)
throws UaException {

var target =
new AliasTarget(
new NodeId(exampleNamespace.getNamespaceIndex(), targetIdentifier).expanded(),
null,
NodeIds.AliasFor);

aliasManager.addAlias(categoryId, aliasName, List.of(target));
}

private Set<EndpointConfig> createEndpointConfigs(X509Certificate certificate) {
Expand Down Expand Up @@ -319,10 +376,30 @@ public OpcUaServer getServer() {
}

public CompletableFuture<OpcUaServer> startup() {
return server.startup();
return server
.startup()
.thenApply(
s -> {
// The standard alias Objects and their FindAlias Method Nodes exist once the
// OpcUaServer is constructed, but the AliasManager is documented to start after
// the server itself has started.
aliasManager.startup();

try {
addDemoAliases();
} catch (UaException e) {
throw new CompletionException(e);
}

return s;
});
}

public CompletableFuture<OpcUaServer> shutdown() {
if (aliasManager.isRunning()) {
aliasManager.shutdown();
}

alarmConditionsNamespace.shutdown();
exampleNamespace.shutdown();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2026 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.aliases;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.eclipse.milo.opcua.sdk.test.AbstractClientServerTest;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.junit.jupiter.api.Test;

/**
* Tests that {@link AliasManager}'s wire entry points reject calls once the manager is no longer
* running.
*
* <p>Deliberately located in the manager's package: the guarded state is only reachable over the
* wire in the narrow race where a dispatched Call loses the manager-lock race to {@code shutdown()}
* (afterwards the Method Nodes are deleted, so a fresh Call never resolves them), which cannot be
* produced deterministically from a client. Calling the package-private entry points on a shut-down
* manager exercises exactly the state that race leaves behind.
*/
class AliasManagerWireEntryShutdownTest extends AbstractClientServerTest {

// WHY: a Call dispatched just before shutdown() can pass authorization and then wait on the
// manager lock; if shutdown wins the lock first, the AddressSpace fragment is unregistered by
// the time the handler proceeds, so mutating would create ghost Nodes and persist spurious
// LastChange values. The entry point must fail with a defined code instead.
@Test
void addAliasEntriesOnShutDownManagerFailsWithBadInvalidState() {
AliasManager manager = newStartedManager();
manager.shutdown();

UaException e =
assertThrows(
UaException.class,
() ->
manager.addAliasEntries(
NodeIds.Aliases,
new String[] {"PostShutdownAddAlias"},
new ExpandedNodeId[] {newNodeId("TestInt32").expanded()},
new String[0],
NodeId.NULL_VALUE));

assertEquals(StatusCode.of(StatusCodes.Bad_InvalidState), e.getStatusCode());
}

// WHY: the same mid-dispatch-vs-shutdown race exists for DeleteAliasesFromCategory, and the
// Delete path additionally touches application NodeManagers, so a post-shutdown call must fail
// with the same defined code instead of mutating ghost state.
@Test
void deleteAliasEntriesOnShutDownManagerFailsWithBadInvalidState() {
AliasManager manager = newStartedManager();
manager.shutdown();

UaException e =
assertThrows(
UaException.class,
() ->
manager.deleteAliasEntries(
NodeIds.Aliases, new String[] {"PostShutdownDeleteAlias"}, null));

assertEquals(StatusCode.of(StatusCodes.Bad_InvalidState), e.getStatusCode());
}

private AliasManager newStartedManager() {
AliasManager manager =
new AliasManager(
server,
AliasManagerConfig.builder()
.configurationEnabled(true)
.nodeNamespaceIndex(testNamespace.getNamespaceIndex())
.build());
manager.startup();
return manager;
}
}
Loading
Loading