This is an automated email from the ASF dual-hosted git repository.

amichair pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/aries-rsa.git

commit d2afc5a70944fd7ce4e27fac07f00f742c5f41e3
Author: Amichai Rothman <[email protected]>
AuthorDate: Sat Jun 6 14:37:15 2026 +0300

    ARIES-2233 Extract common LocalEndpointManager to track local endpoints
---
 .../aries/rsa/discovery/mdns/MdnsDiscovery.java    |   2 +-
 .../discovery/mdns/PublishingEndpointListener.java | 112 ++-----------
 .../rsa/discovery/tcp/TcpConnectionManager.java    |  14 +-
 .../aries/rsa/discovery/tcp/TcpDiscovery.java      |  26 +--
 .../zookeeper/PublishingEndpointListener.java      |  37 +----
 .../zookeeper/PublishingEndpointListenerTest.java  |  65 --------
 .../rsa/spi/discovery/LocalEndpointManager.java    | 181 +++++++++++++++++++++
 7 files changed, 219 insertions(+), 218 deletions(-)

diff --git 
a/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/MdnsDiscovery.java
 
b/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/MdnsDiscovery.java
index bee88eb2..b97141d0 100644
--- 
a/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/MdnsDiscovery.java
+++ 
b/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/MdnsDiscovery.java
@@ -80,7 +80,7 @@ public class MdnsDiscovery {
         interestManager = new InterestManager(eventSourceFactory, parser, 
client);
         interestManager.start(ctx, null);
         fwUuid = ctx.getProperty(FRAMEWORK_UUID);
-        publishingListener = new PublishingEndpointListener(parser, ctx, 
fwUuid);
+        publishingListener = new PublishingEndpointListener(parser, ctx);
     }
 
     @Reference(policy = ReferencePolicy.DYNAMIC)
diff --git 
a/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/PublishingEndpointListener.java
 
b/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/PublishingEndpointListener.java
index bd46f37a..08fae8c7 100644
--- 
a/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/PublishingEndpointListener.java
+++ 
b/discovery/mdns/src/main/java/org/apache/aries/rsa/discovery/mdns/PublishingEndpointListener.java
@@ -18,18 +18,13 @@
  */
 package org.apache.aries.rsa.discovery.mdns;
 
-import static java.util.Collections.singleton;
-import static java.util.stream.Collectors.toSet;
 import static javax.ws.rs.core.MediaType.SERVER_SENT_EVENTS;
-import static org.apache.aries.rsa.util.CollectionUtils.union;
 
 import java.io.ByteArrayOutputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.Dictionary;
 import java.util.Hashtable;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -39,17 +34,14 @@ import javax.ws.rs.sse.Sse;
 import javax.ws.rs.sse.SseEventSink;
 
 import org.apache.aries.rsa.spi.EndpointDescriptionParser;
-import org.osgi.framework.Bundle;
+import org.apache.aries.rsa.spi.discovery.LocalEndpointManager;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.Constants;
-import org.osgi.framework.ServiceFactory;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.jaxrs.whiteboard.annotations.RequireJaxrsWhiteboard;
 import org.osgi.service.remoteserviceadmin.EndpointDescription;
 import org.osgi.service.remoteserviceadmin.EndpointEvent;
 import org.osgi.service.remoteserviceadmin.EndpointEventListener;
-import org.osgi.service.remoteserviceadmin.RemoteConstants;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,80 +54,48 @@ public class PublishingEndpointListener {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(PublishingEndpointListener.class);
 
-    private final String uuid;
-
     private final EndpointDescriptionParser parser;
 
-    private final ServiceRegistration<?> listenerReg;
-    private final ServiceRegistration<?> resourceReg;
+    private final LocalEndpointManager localEndpointManager;
 
-    private final ConcurrentMap<String, SponsoredEndpoint> localEndpoints = 
new ConcurrentHashMap<>();
+    private final ServiceRegistration<?> resourceReg;
 
     private final Set<Subscription> listeners = ConcurrentHashMap.newKeySet();
 
     @SuppressWarnings("serial")
-    public PublishingEndpointListener(EndpointDescriptionParser parser, 
BundleContext bctx, String uuid) {
+    public PublishingEndpointListener(EndpointDescriptionParser parser, 
BundleContext context) {
         this.parser = parser;
-        this.uuid = uuid;
-        String[] ifAr = { EndpointEventListener.class.getName() };
-        Dictionary<String, Object> props = serviceProperties(uuid);
-        listenerReg = bctx.registerService(ifAr, new ListenerFactory(), props);
-        resourceReg = bctx.registerService(PublishingEndpointListener.class, 
this,
-                new Hashtable<>() {{put("osgi.jaxrs.resource", 
Boolean.TRUE);}});
+        this.localEndpointManager = new LocalEndpointManager();
+        localEndpointManager.setListener((event, filter) -> 
endpointUpdate(event.getEndpoint(), event.getType()));
+        localEndpointManager.start(context, getClass().getName());
+        resourceReg = 
context.registerService(PublishingEndpointListener.class, this,
+            new Hashtable<>() {{put("osgi.jaxrs.resource", Boolean.TRUE);}});
     }
 
     @Deactivate
     public void stop() {
-        listenerReg.unregister();
+        localEndpointManager.stop();
         listeners.forEach(Subscription::close);
         resourceReg.unregister();
     }
 
-    private void endpointUpdate(Long bundleId, EndpointDescription ed, int 
type) {
-        String edFwUuid = ed.getFrameworkUUID();
-        if (edFwUuid == null || !edFwUuid.equals(uuid)) {
-            LOG.warn("This listener has been called with an endpoint {} for a 
remote framework {}", ed.getId(), edFwUuid);
-            return;
-        }
+    private void endpointUpdate(EndpointDescription ed, int type) {
         String id = ed.getId();
         switch(type) {
             case EndpointEvent.ADDED:
             case EndpointEvent.MODIFIED:
-                localEndpoints.compute(id, (k,v) -> new SponsoredEndpoint(ed,
-                    v == null
-                    ? singleton(bundleId)
-                    : union(v.sponsors, singleton(bundleId))));
                 String data = toEndpointData(ed);
                 listeners.forEach(s -> s.update(data));
                 break;
             case EndpointEvent.MODIFIED_ENDMATCH:
             case EndpointEvent.REMOVED:
-                boolean act = localEndpoints.compute(id, (k,v) -> {
-                    if (v == null) {
-                        return null;
-                    } else {
-                      Set<Long> updated = v.sponsors.stream().filter(l -> 
!bundleId.equals(l)).collect(toSet());
-                      return updated.isEmpty() ? null : new 
SponsoredEndpoint(v.ed, updated);
-                    }
-                }) == null;
-
-                if (act) {
-                    listeners.forEach(s -> s.revoke(id));
-                }
+                listeners.forEach(s -> s.revoke(id));
                 break;
             default:
                 LOG.error("Unknown event type {} for endpoint {}", type, ed);
         }
     }
 
-    private Dictionary<String, Object> serviceProperties(String uuid) {
-        String scope = String.format("(&(%s=*)(%s=%s))", Constants.OBJECTCLASS,
-                        RemoteConstants.ENDPOINT_FRAMEWORK_UUID, uuid);
-        Dictionary<String, Object> props = new Hashtable<>();
-        props.put(EndpointEventListener.ENDPOINT_LISTENER_SCOPE, scope);
-        return props;
-    }
-
     private String toEndpointData(EndpointDescription ed) {
         try {
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -154,45 +114,11 @@ public class PublishingEndpointListener {
         Subscription subscription = new Subscription(sse, sink);
         listeners.add(subscription);
 
-        localEndpoints.values().stream()
-            .map(s -> toEndpointData(s.ed))
+        localEndpointManager.getEndpoints().stream()
+            .map(this::toEndpointData)
             .forEach(subscription::update);
     }
 
-    private class ListenerFactory implements 
ServiceFactory<PerClientEndpointEventListener> {
-
-        @Override
-        public PerClientEndpointEventListener getService(Bundle bundle,
-                ServiceRegistration<PerClientEndpointEventListener> 
registration) {
-            return new PerClientEndpointEventListener(bundle.getBundleId());
-        }
-
-        @Override
-        public void ungetService(Bundle bundle, 
ServiceRegistration<PerClientEndpointEventListener> registration,
-                PerClientEndpointEventListener service) {
-            Long bundleId = service.bundleId;
-            localEndpoints.values().stream()
-                .filter(s -> s.sponsors.contains(bundleId))
-                .forEach(s -> endpointUpdate(bundleId, s.ed, 
EndpointEvent.REMOVED));
-        }
-
-    }
-
-    private class PerClientEndpointEventListener implements 
EndpointEventListener {
-
-        private final Long bundleId;
-
-        public PerClientEndpointEventListener(Long bundleId) {
-            super();
-            this.bundleId = bundleId;
-        }
-
-        @Override
-        public void endpointChanged(EndpointEvent event, String filter) {
-            endpointUpdate(bundleId, event.getEndpoint(), event.getType());
-        }
-    }
-
     class Subscription {
 
         static final String ENDPOINT_UPDATED = "UPDATED";
@@ -229,14 +155,4 @@ public class PublishingEndpointListener {
             }
         }
     }
-
-    private static class SponsoredEndpoint {
-        private final EndpointDescription ed;
-        private final Set<Long> sponsors;
-
-        public SponsoredEndpoint(EndpointDescription ed, Set<Long> sponsors) {
-            this.ed = ed;
-            this.sponsors = sponsors;
-        }
-    }
 }
diff --git 
a/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpConnectionManager.java
 
b/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpConnectionManager.java
index d5623cb1..4962a16c 100644
--- 
a/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpConnectionManager.java
+++ 
b/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpConnectionManager.java
@@ -20,6 +20,7 @@ package org.apache.aries.rsa.discovery.tcp;
 
 import org.apache.aries.rsa.discovery.tcp.TcpMessage.*;
 import org.apache.aries.rsa.spi.discovery.InterestManager;
+import org.apache.aries.rsa.spi.discovery.LocalEndpointManager;
 import org.osgi.service.remoteserviceadmin.EndpointDescription;
 import org.osgi.service.remoteserviceadmin.EndpointEvent;
 import org.osgi.service.remoteserviceadmin.EndpointEventListener;
@@ -51,6 +52,7 @@ public class TcpConnectionManager implements 
EndpointEventListener {
     private static final Logger LOG = 
LoggerFactory.getLogger(TcpConnectionManager.class);
 
     private final InterestManager interestManager;
+    private final LocalEndpointManager localEndpointManager;
     private final String localAddress;
     private final String localUuid;
     private final long reconnectDelay;
@@ -59,15 +61,16 @@ public class TcpConnectionManager implements 
EndpointEventListener {
     private final ExecutorService executor = Executors.newCachedThreadPool(); 
// for connect/accept threads
     private final Set<TcpConnection> connections = 
ConcurrentHashMap.newKeySet(); // all connections, including before handshake
     private final Map<String, TcpConnection> connectionsByUuid = new 
ConcurrentHashMap<>(); // connections after handshake (known uuid)
-    private final Map<String, EndpointDescription> localEndpoints = new 
ConcurrentHashMap<>();
     private final Set<String> peers = ConcurrentHashMap.newKeySet(); // all 
configured and discovered (gossip) peer addresses
 
     private ServerSocket serverSocket;
     private volatile boolean closing;
 
-    public TcpConnectionManager(InterestManager interestManager, String 
localAddress,
-            String localUuid, long reconnectDelay, boolean gossip) {
+    public TcpConnectionManager(InterestManager interestManager, 
LocalEndpointManager localEndpointManager,
+            String localAddress, String localUuid, long reconnectDelay, 
boolean gossip) {
         this.interestManager = interestManager;
+        this.localEndpointManager = localEndpointManager;
+        localEndpointManager.setListener(this);
         this.localAddress = localAddress;
         this.localUuid = localUuid;
         this.reconnectDelay = reconnectDelay;
@@ -214,7 +217,8 @@ public class TcpConnectionManager implements 
EndpointEventListener {
                 return;
             }
             // send all of our known local endpoints to the new peer
-            localEndpoints.values().forEach(endpoint -> conn.send(new 
UpdateMessage(endpoint.getProperties())));
+            localEndpointManager.getEndpoints().forEach(
+                endpoint -> conn.send(new 
UpdateMessage(endpoint.getProperties())));
         } else if (message instanceof UpdateMessage) {
             UpdateMessage u = (UpdateMessage) message;
             EndpointDescription endpoint = new 
EndpointDescription(u.getProperties());
@@ -236,12 +240,10 @@ public class TcpConnectionManager implements 
EndpointEventListener {
         switch (event.getType()) {
             case EndpointEvent.ADDED:
             case EndpointEvent.MODIFIED:
-                localEndpoints.put(endpointId, endpoint);
                 message = new UpdateMessage(endpoint.getProperties());
                 break;
             case EndpointEvent.MODIFIED_ENDMATCH:
             case EndpointEvent.REMOVED:
-                localEndpoints.remove(endpointId);
                 message = new RemoveMessage(endpointId);
                 break;
             default: throw new RuntimeException("Unknown event type: " + 
event.getType());
diff --git 
a/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpDiscovery.java
 
b/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpDiscovery.java
index e717bec5..49606523 100644
--- 
a/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpDiscovery.java
+++ 
b/discovery/tcp/src/main/java/org/apache/aries/rsa/discovery/tcp/TcpDiscovery.java
@@ -20,8 +20,8 @@ package org.apache.aries.rsa.discovery.tcp;
 
 import org.apache.aries.rsa.annotations.RSADiscoveryProvider;
 import org.apache.aries.rsa.spi.discovery.InterestManager;
+import org.apache.aries.rsa.spi.discovery.LocalEndpointManager;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
@@ -36,14 +36,10 @@ import java.lang.reflect.Proxy;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Arrays;
-import java.util.Dictionary;
-import java.util.Hashtable;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
 import static org.osgi.framework.Constants.FRAMEWORK_UUID;
-import static 
org.osgi.service.remoteserviceadmin.EndpointEventListener.ENDPOINT_LISTENER_SCOPE;
-import static 
org.osgi.service.remoteserviceadmin.RemoteConstants.ENDPOINT_FRAMEWORK_UUID;
 
 /**
  * The main TCP Discovery provider component.
@@ -72,11 +68,12 @@ public class TcpDiscovery {
 
     private InterestManager interestManager;
     private TcpConnectionManager connectionManager;
-    private ServiceRegistration<?> listenerRegistration;
+    private LocalEndpointManager localEndpointManager;
 
     public TcpDiscovery() {
         // initialize in constructor before we start getting reference bind 
events
         interestManager = new InterestManager();
+        localEndpointManager = new LocalEndpointManager();
     }
 
     public static URI toURI(String address) {
@@ -151,18 +148,10 @@ public class TcpDiscovery {
         URI uri = toURI(address);
         bindAddress = bindAddress == null ? uri.getHost() : 
toURI(bindAddress).getHost(); // just the host
         connectionManager = new TcpConnectionManager(
-            interestManager, address, uuid, config.reconnectDelay(), 
config.gossip());
+            interestManager, localEndpointManager, address, uuid, 
config.reconnectDelay(), config.gossip());
         connectionManager.open(bindAddress, uri.getPort(), 
Arrays.asList(peers));
     }
 
-    private void registerListener(BundleContext context, String uuid) {
-        Dictionary<String, Object> props = new Hashtable<>();
-        props.put(OWN_LISTENER_PROP, Boolean.TRUE); // mark our own listener 
for exclusion
-        String scope = "(&(objectClass=*)(" + ENDPOINT_FRAMEWORK_UUID + "=" + 
uuid + "))";
-        props.put(ENDPOINT_LISTENER_SCOPE, scope);
-        listenerRegistration = 
context.registerService(EndpointEventListener.class, connectionManager, props);
-    }
-
     @Activate
     void start(BundleContext context, Config config) {
         String uuid = context.getProperty(FRAMEWORK_UUID);
@@ -170,18 +159,17 @@ public class TcpDiscovery {
         LOG.info("Starting TCP discovery for framework {} with config {}", 
uuid, config);
         try {
             initConnectionManager(config, uuid);
-            // register ourselves to capture local endpoint exports
-            registerListener(context, uuid);
         } catch (IOException ioe) {
             LOG.error("failed to start TCP connection manager", ioe);
         }
         interestManager.start(context, OWN_LISTENER_PROP);
+        localEndpointManager.start(context, OWN_LISTENER_PROP);
     }
 
     @Deactivate
     void stop() throws IOException {
-        if (listenerRegistration != null) {
-            listenerRegistration.unregister();
+        if (localEndpointManager != null) {
+            localEndpointManager.stop();
         }
         if (connectionManager != null) {
             connectionManager.close();
diff --git 
a/discovery/zookeeper/src/main/java/org/apache/aries/rsa/discovery/zookeeper/PublishingEndpointListener.java
 
b/discovery/zookeeper/src/main/java/org/apache/aries/rsa/discovery/zookeeper/PublishingEndpointListener.java
index db6d35a0..239a9ea5 100644
--- 
a/discovery/zookeeper/src/main/java/org/apache/aries/rsa/discovery/zookeeper/PublishingEndpointListener.java
+++ 
b/discovery/zookeeper/src/main/java/org/apache/aries/rsa/discovery/zookeeper/PublishingEndpointListener.java
@@ -18,21 +18,16 @@
  */
 package org.apache.aries.rsa.discovery.zookeeper;
 
-import java.util.Dictionary;
-import java.util.Hashtable;
-
 import org.apache.aries.rsa.discovery.zookeeper.client.ClientManager;
 import 
org.apache.aries.rsa.discovery.zookeeper.client.ZookeeperEndpointRepository;
+import org.apache.aries.rsa.spi.discovery.LocalEndpointManager;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.Constants;
-import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.remoteserviceadmin.EndpointEvent;
 import org.osgi.service.remoteserviceadmin.EndpointEventListener;
-import org.osgi.service.remoteserviceadmin.RemoteConstants;
 
 /**
  * Listens for local {@link EndpointEvent}s using {@link EndpointEventListener}
@@ -40,38 +35,22 @@ import org.osgi.service.remoteserviceadmin.RemoteConstants;
  */
 @SuppressWarnings("deprecation")
 @Component(service = {}, immediate = true)
-public class PublishingEndpointListener implements EndpointEventListener {
+public class PublishingEndpointListener {
 
-    private ServiceRegistration<?> listenerReg;
+    protected LocalEndpointManager localEndpointManager;
 
     @Reference
     private ZookeeperEndpointRepository repository;
 
     @Activate
-    public void start(BundleContext bctx) {
-        String uuid = bctx.getProperty(Constants.FRAMEWORK_UUID);
-        String[] ifAr = { EndpointEventListener.class.getName() };
-        Dictionary<String, String> props = serviceProperties(uuid);
-        listenerReg = bctx.registerService(ifAr, this, props);
+    public void start(BundleContext context) {
+        localEndpointManager = new LocalEndpointManager();
+        localEndpointManager.setListener((event, filter) -> 
repository.endpointChanged(event));
+        localEndpointManager.start(context, 
ClientManager.DISCOVERY_ZOOKEEPER_ID);
     }
 
     @Deactivate
     public void stop() {
-        listenerReg.unregister();
-    }
-
-    @Override
-    public void endpointChanged(EndpointEvent event, String filter) {
-        repository.endpointChanged(event);
+        localEndpointManager.stop();
     }
-
-    private Dictionary<String, String> serviceProperties(String uuid) {
-        String scope = String.format("(&(%s=*)(%s=%s))", Constants.OBJECTCLASS,
-                        RemoteConstants.ENDPOINT_FRAMEWORK_UUID, uuid);
-        Dictionary<String, String> props = new Hashtable<>();
-        props.put(EndpointEventListener.ENDPOINT_LISTENER_SCOPE, scope);
-        props.put(ClientManager.DISCOVERY_ZOOKEEPER_ID, "true");
-        return props;
-    }
-
 }
diff --git 
a/discovery/zookeeper/src/test/java/org/apache/aries/rsa/discovery/zookeeper/PublishingEndpointListenerTest.java
 
b/discovery/zookeeper/src/test/java/org/apache/aries/rsa/discovery/zookeeper/PublishingEndpointListenerTest.java
deleted file mode 100644
index 89987b22..00000000
--- 
a/discovery/zookeeper/src/test/java/org/apache/aries/rsa/discovery/zookeeper/PublishingEndpointListenerTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.aries.rsa.discovery.zookeeper;
-
-import static org.mockito.Mockito.verify;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import 
org.apache.aries.rsa.discovery.zookeeper.client.ZookeeperEndpointRepository;
-import org.apache.zookeeper.KeeperException;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.osgi.framework.Constants;
-import org.osgi.service.remoteserviceadmin.EndpointDescription;
-import org.osgi.service.remoteserviceadmin.EndpointEvent;
-import org.osgi.service.remoteserviceadmin.RemoteConstants;
-
-@RunWith(MockitoJUnitRunner.class)
-public class PublishingEndpointListenerTest {
-    @Mock
-    ZookeeperEndpointRepository repository;
-
-    @InjectMocks
-    PublishingEndpointListener eli;
-
-    @Test
-    public void testEndpointRemovalAdding() throws KeeperException, 
InterruptedException {
-        EndpointDescription endpoint = createEndpoint();
-        EndpointEvent event1 = new EndpointEvent(EndpointEvent.ADDED, 
endpoint);
-        eli.endpointChanged(event1, null);
-        EndpointEvent event2 = new EndpointEvent(EndpointEvent.REMOVED, 
endpoint);
-        eli.endpointChanged(event2, null);
-
-        verify(repository).endpointChanged(event1);
-        verify(repository).endpointChanged(event2);
-    }
-
-    private EndpointDescription createEndpoint() {
-        Map<String, Object> props = new HashMap<>();
-        props.put(Constants.OBJECTCLASS, new String[] {"myClass"});
-        props.put(RemoteConstants.ENDPOINT_ID, "http://google.de:80/test/sub";);
-        props.put(RemoteConstants.SERVICE_IMPORTED_CONFIGS, "myConfig");
-        return new EndpointDescription(props);
-    }
-}
diff --git 
a/spi/src/main/java/org/apache/aries/rsa/spi/discovery/LocalEndpointManager.java
 
b/spi/src/main/java/org/apache/aries/rsa/spi/discovery/LocalEndpointManager.java
new file mode 100644
index 00000000..b9f4d6ae
--- /dev/null
+++ 
b/spi/src/main/java/org/apache/aries/rsa/spi/discovery/LocalEndpointManager.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.aries.rsa.spi.discovery;
+
+import org.osgi.framework.*;
+import org.osgi.service.remoteserviceadmin.EndpointDescription;
+import org.osgi.service.remoteserviceadmin.EndpointEvent;
+import org.osgi.service.remoteserviceadmin.EndpointEventListener;
+import org.osgi.service.remoteserviceadmin.RemoteConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.singleton;
+import static org.apache.aries.rsa.util.CollectionUtils.union;
+
+/**
+ * Keeps track of all local endpoints so that updates about them can be sent to
+ * remote systems.
+ * <p>
+ * This includes registering an EndpointEventListener with a scope that 
excludes all
+ * non-local endpoints, a special service property that uniquely identifies 
this
+ * instance so that the interest manager can exclude it from its tracking, and 
a
+ * ServiceFactory that keeps track of which bundles notified us about which 
endpoints
+ * so that when a bundle is stopped all of its endpoints will be removed, as 
the spec
+ * requires.
+ * <p>
+ * After being processed here, endpoint events are forwarded to a supplied 
listener
+ * for implementation-specific handling (e.g. transmitting updates to remote 
hosts).
+ */
+public class LocalEndpointManager {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(LocalEndpointManager.class);
+
+    protected static class EndpointBundles {
+        EndpointDescription endpoint;
+        Set<Long> bundleIds;
+
+        EndpointBundles(EndpointDescription endpoint, Set<Long> bundleIds) {
+            this.endpoint = endpoint;
+            this.bundleIds = bundleIds;
+        }
+    }
+
+    protected ServiceRegistration<EndpointEventListener> listenerRegistration;
+    // local endpoint ids and their respective endpoints and the bundles that 
notified us about them
+    protected Map<String, EndpointBundles> localEndpoints = new 
ConcurrentHashMap<>();
+    // the listener to which endpoint events are delegated after being 
processed
+    protected EndpointEventListener listener;
+
+    public Collection<EndpointDescription> getEndpoints() {
+        return localEndpoints.values().stream().map(eb -> 
eb.endpoint).collect(Collectors.toList());
+    }
+
+    public void setListener(EndpointEventListener listener) {
+        this.listener = listener;
+    }
+
+    public void start(BundleContext context, String excludeProperty) {
+        listenerRegistration = registerListener(context, excludeProperty);
+    }
+
+    public void stop() {
+        if (listenerRegistration  != null) {
+            listenerRegistration.unregister();
+        }
+    }
+
+    /**
+     * Registers an EndpointEventListener so we get notified of all local 
endpoints,
+     * with a few special features:
+     * <ul>
+     * <li>It is registered with a special given property that uniquely 
identifies
+     * this instance in order for the interest manager to exclude it in its 
scope
+     * when listening for interests</li>
+     * <li>It is registered with a scope that excludes all non-local endpoints
+     * by requiring the framework ID to be equal to the given context's 
framework ID</li>
+     * <li>It is registered as a {@link ServiceFactory} in order to keep track 
of
+     * which bundle(s) notifies us of each endpoint, so that when the bundle is
+     * stopped we can remove those endpoints (as required by the spec)</li>
+     * </ul>
+     *
+     * @param context the registering bundle context
+     * @param excludeProperty the name of a service property that uniquely 
identifies
+     *        this instance
+     * @return the listener service registration
+     */
+    protected ServiceRegistration<EndpointEventListener> registerListener(
+            BundleContext context, String excludeProperty) {
+        Dictionary<String, Object> props = new Hashtable<>();
+        props.put(excludeProperty, Boolean.TRUE); // mark our own listener for 
exclusion
+        String uuid = context.getProperty(Constants.FRAMEWORK_UUID);
+        String scope = String.format("(&(%s=*)(%s=%s))", Constants.OBJECTCLASS,
+                RemoteConstants.ENDPOINT_FRAMEWORK_UUID, uuid);
+        props.put(EndpointEventListener.ENDPOINT_LISTENER_SCOPE, scope);
+        LOG.debug("registering EndpointEventListener factory with properties 
{}", props);
+        return context.registerService(EndpointEventListener.class, new 
ListenerServiceFactory(), props);
+    }
+
+    /**
+     * A {@link ServiceFactory} registered as an {@link EndpointEventListener} 
so that we can
+     * keep track of which bundle(s) notified us about every endpoint.
+     */
+    protected class ListenerServiceFactory implements 
ServiceFactory<EndpointEventListener> {
+
+        @Override
+        public EndpointEventListener getService(Bundle bundle, 
ServiceRegistration<EndpointEventListener> registration) {
+            long bundleId = bundle.getBundleId();
+            LOG.debug("service factory getting EndpointEventListener for 
bundle {} ({})",
+                bundleId, bundle.getSymbolicName());
+            return ((event, filter) -> endpointChanged(event, filter, 
bundleId));
+        }
+
+        @Override
+        public void ungetService(Bundle bundle, 
ServiceRegistration<EndpointEventListener> registration, EndpointEventListener 
service) {
+            long bundleId = bundle.getBundleId();
+            LOG.debug("service factory ungetting EndpointEventListener for 
bundle {} ({})",
+                bundleId, bundle.getSymbolicName());
+            localEndpoints.values().stream()
+                .filter(eb -> eb.bundleIds.contains(bundleId))
+                .forEach(eb -> endpointChanged(new 
EndpointEvent(EndpointEvent.REMOVED, eb.endpoint), "", bundleId));
+        }
+    }
+
+    /**
+     * Notification that an endpoint has changed.
+     * <p>
+     * This method is similar to {@link EndpointEventListener#endpointChanged},
+     * with the added bundle ID of the bundle invoking the listener (which
+     * we provide by using a ServiceFactory).
+     *
+     * @param event The event containing the details about the change.
+     * @param filter The filter from the {@link 
EndpointEventListener#ENDPOINT_LISTENER_SCOPE}
+     *        that matches (or for {@link EndpointEvent#MODIFIED_ENDMATCH} and
+     *        {@link EndpointEvent#REMOVED} used to match) the endpoint, or an 
empty
+     *        string when removing the endpoint because the notifying bundle 
was stopped
+     */
+    public void endpointChanged(EndpointEvent event, String filter, Long 
bundleId) {
+        EndpointDescription endpoint = event.getEndpoint();
+        LOG.debug("got endpointChanged event of type {} from bundle {}: {}",
+            event.getType(), bundleId, endpoint);
+        switch (event.getType()) {
+            case EndpointEvent.ADDED:
+            case EndpointEvent.MODIFIED:
+                localEndpoints.compute(endpoint.getId(),
+                    (e, eb) -> new EndpointBundles(endpoint,
+                        eb == null ? singleton(bundleId) : union(eb.bundleIds, 
singleton(bundleId))));
+                break;
+            case EndpointEvent.REMOVED:
+            case EndpointEvent.MODIFIED_ENDMATCH:
+                localEndpoints.computeIfPresent(endpoint.getId(),
+                    (e, eb) -> {
+                        Set<Long> bundleIds = eb.bundleIds.stream()
+                            .filter(id -> !bundleId.equals(id))
+                            .collect(Collectors.toSet());
+                        return bundleIds.isEmpty() ? null : new 
EndpointBundles(endpoint, bundleIds);
+                    });
+                break;
+        }
+        listener.endpointChanged(event, filter);
+    }
+}

Reply via email to