diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/OpcUaServer.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/OpcUaServer.java index 685082520a..c826a3d5af 100644 --- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/OpcUaServer.java +++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/OpcUaServer.java @@ -19,9 +19,9 @@ import java.security.KeyPair; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -857,8 +857,8 @@ private short getSecurityLevel( private static class ServerEventNotifier implements EventNotifier { - private final List eventListeners = - Collections.synchronizedList(new ArrayList<>()); + private final Set eventListeners = + Collections.synchronizedSet(new LinkedHashSet<>()); @Override public void fire(BaseEventTypeNode event) { diff --git a/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/ServerEventNotifierTest.java b/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/ServerEventNotifierTest.java new file mode 100644 index 0000000000..218a5aedbc --- /dev/null +++ b/opc-ua-sdk/sdk-server/src/test/java/org/eclipse/milo/opcua/sdk/server/ServerEventNotifierTest.java @@ -0,0 +1,58 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Constructor; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +public class ServerEventNotifierTest { + + @Test + public void duplicateRegistrationsAreIgnored() throws Exception { + EventNotifier notifier = newServerEventNotifier(); + AtomicInteger eventCount = new AtomicInteger(); + EventListener listener = event -> eventCount.incrementAndGet(); + + notifier.register(listener); + notifier.register(listener); + notifier.register(listener); + + notifier.fire(null); + + assertEquals(1, eventCount.get()); + } + + @Test + public void unregisterRemovesDuplicateRegistration() throws Exception { + EventNotifier notifier = newServerEventNotifier(); + AtomicInteger eventCount = new AtomicInteger(); + EventListener listener = event -> eventCount.incrementAndGet(); + + notifier.register(listener); + notifier.register(listener); + notifier.unregister(listener); + + notifier.fire(null); + + assertEquals(0, eventCount.get()); + } + + private static EventNotifier newServerEventNotifier() throws Exception { + Class notifierClass = Class.forName(OpcUaServer.class.getName() + "$ServerEventNotifier"); + Constructor constructor = notifierClass.getDeclaredConstructor(); + constructor.setAccessible(true); + + return (EventNotifier) constructor.newInstance(); + } +}