partial hook implementation (findhook, eventlistenerhook)
diff --git a/winegrower-core/src/main/java/org/apache/winegrower/deployer/BundleContextImpl.java b/winegrower-core/src/main/java/org/apache/winegrower/deployer/BundleContextImpl.java
index 7fc2f80..9d823a0 100644
--- a/winegrower-core/src/main/java/org/apache/winegrower/deployer/BundleContextImpl.java
+++ b/winegrower-core/src/main/java/org/apache/winegrower/deployer/BundleContextImpl.java
@@ -22,8 +22,8 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
+import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;
@@ -48,8 +48,13 @@
import org.osgi.framework.ServiceObjects;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.hooks.service.FindHook;
+import org.slf4j.LoggerFactory;
public class BundleContextImpl implements BundleContext {
+ private static final ServiceReference<?>[] EMPTY_REFS = new ServiceReference<?>[0];
+ private static final Bundle[] EMPTY_BUNDLES = new Bundle[0];
+
private final Manifest manifest;
private final OSGiServices services;
private final Supplier<Bundle> bundleSupplier;
@@ -108,17 +113,28 @@
@Override
public Bundle getBundle(final long id) {
- return ofNullable(registry.getBundles().get(id)).map(OSGiBundleLifecycle::getBundle).orElse(null);
+ return ofNullable(registry.getBundles().get(id))
+ .map(OSGiBundleLifecycle::getBundle)
+ .map(bundle -> {
+ final List<Bundle> bundles = Stream.of(bundle).collect(toList());
+ invokeBundleFinHooks(bundles);
+ return bundles.isEmpty() ? null : bundle;
+ })
+ .orElse(null);
}
@Override
public Bundle[] getBundles() {
- return registry.getBundles().values().stream().map(OSGiBundleLifecycle::getBundle).toArray(Bundle[]::new);
+ final List<Bundle> bundles = registry.getBundles().values().stream()
+ .map(OSGiBundleLifecycle::getBundle)
+ .collect(toList());
+ invokeBundleFinHooks(bundles);
+ return bundles.toArray(EMPTY_BUNDLES);
}
@Override
public void addServiceListener(final ServiceListener listener, final String filter) {
- services.addListener(listener, filter == null ? null : createFilter(filter));
+ services.addListener(listener, filter == null ? null : createFilter(filter), this);
}
@Override
@@ -173,39 +189,12 @@
@Override
public ServiceReference<?>[] getServiceReferences(final String clazz, final String filter) {
- final Filter predicate = filter == null ? null : createFilter(filter);
- final Bundle bundle = getBundle();
- final Class<?> expected;
- try {
- expected = clazz == null ? Object.class : bundle.loadClass(clazz);
- } catch (final ClassNotFoundException e) {
- return new ServiceReference<?>[0];
- }
- return services.getServices().stream()
- .filter(it -> Stream.of(ServiceRegistrationImpl.class.cast(it).getClasses())
- .map(name -> {
- try {
- return bundle.loadClass(name);
- } catch (final NoClassDefFoundError | ClassNotFoundException e) {
- return null;
- }
- })
- .filter(Objects::nonNull)
- .anyMatch(expected::isAssignableFrom))
- .filter(it -> predicate == null || predicate.match(it.getReference()))
- .map(it -> ServiceRegistrationImpl.class.cast(it).getReference())
- .toArray(ServiceReference[]::new);
+ return doGetReferences(clazz, filter, true);
}
@Override
public ServiceReference<?>[] getAllServiceReferences(final String clazz, final String filter) {
- final Filter predicate = filter == null ? null : createFilter(filter);
- return services.getServices().stream()
- .map(ServiceRegistrationImpl.class::cast)
- .filter(it -> it.getClasses() != null && asList(it.getClasses()).contains(clazz))
- .filter(it -> predicate == null || predicate.match(it.getReference()))
- .map(ServiceRegistration::getReference)
- .toArray(ServiceReference[]::new);
+ return doGetReferences(clazz, filter, false);
}
@Override
@@ -221,10 +210,8 @@
@Override
public <S> Collection<ServiceReference<S>> getServiceReferences(final Class<S> clazz, final String filter) {
- final Filter predicate = filter == null ? null : createFilter(filter);
- return Arrays.stream(getAllServiceReferences(clazz.getName(), filter))
- .map(it ->(ServiceReference<S>) it)
- .filter(it -> predicate == null || predicate.match(it))
+ return Arrays.stream(doGetReferences(clazz.getName(), filter, true))
+ .map(it -> (ServiceReference<S>) it)
.collect(toList());
}
@@ -277,4 +264,53 @@
public Bundle getBundle(final String location) {
return bundleSupplier.get();
}
+
+ private ServiceReference<?>[] doGetReferences(final String clazz, final String filter, final boolean checkAssignable) {
+ final Filter predicate = filter == null ? null : createFilter(filter);
+ final List<ServiceReference> references = services.getServices().stream()
+ .map(ServiceRegistrationImpl.class::cast)
+ .filter(it -> it.getClasses() != null && asList(it.getClasses()).contains(clazz))
+ .filter(it -> predicate == null || predicate.match(it.getReference()))
+ .map(ServiceRegistration::getReference)
+ .collect(toList());
+ invokeServiceFindHooks(clazz, filter, checkAssignable, references);
+ return references.toArray(EMPTY_REFS);
+ }
+
+ private void invokeServiceFindHooks(final String clazz, final String filter,
+ final boolean checkAssignable, final List<ServiceReference> references) {
+ final Collection<ServiceReference<FindHook>> findHooks = services.getHooks().getServiceFindHooks();
+ if (!references.isEmpty() && !findHooks.isEmpty()) {
+ findHooks.forEach(hook -> {
+ final FindHook fh = getService(hook);
+ if (fh != null) {
+ try {
+ fh.find(getBundle().getBundleContext(), clazz, filter, !checkAssignable, Collection.class.cast(references));
+ } catch (final Throwable th) {
+ LoggerFactory.getLogger(BundleContextImpl.class).warn("Can't call '{}'", hook, th);
+ } finally {
+ ungetService(hook);
+ }
+ }
+ });
+ }
+ }
+
+ private void invokeBundleFinHooks(final List<Bundle> bundles) {
+ final Collection<ServiceReference<org.osgi.framework.hooks.bundle.FindHook>> findHooks = services.getHooks().getBundleFindHooks();
+ if (!bundles.isEmpty() && !findHooks.isEmpty()) {
+ findHooks.forEach(hook -> {
+ final org.osgi.framework.hooks.bundle.FindHook fh = getService(hook);
+ if (fh != null) {
+ try {
+ fh.find(getBundle().getBundleContext(), bundles);
+ } catch (final Throwable th) {
+ LoggerFactory.getLogger(BundleContextImpl.class).warn("Can't call '{}'", hook, th);
+ } finally {
+ ungetService(hook);
+ }
+ }
+ });
+ }
+ }
}
diff --git a/winegrower-core/src/main/java/org/apache/winegrower/service/Hooks.java b/winegrower-core/src/main/java/org/apache/winegrower/service/Hooks.java
new file mode 100644
index 0000000..e592d4e
--- /dev/null
+++ b/winegrower-core/src/main/java/org/apache/winegrower/service/Hooks.java
@@ -0,0 +1,39 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.winegrower.service;
+
+import java.util.Collection;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.bundle.FindHook;
+import org.osgi.framework.hooks.service.EventListenerHook;
+
+public class Hooks {
+ private final Collection<ServiceReference<EventListenerHook>> eventListenerHooks = new CopyOnWriteArrayList<>();
+ private final Collection<ServiceReference<FindHook>> bundleFindHooks = new CopyOnWriteArrayList<>();
+ private final Collection<ServiceReference<org.osgi.framework.hooks.service.FindHook>> serviceFindHooks = new CopyOnWriteArrayList<>();
+
+ public Collection<ServiceReference<EventListenerHook>> getEventListenerHooks() {
+ return eventListenerHooks;
+ }
+
+ public Collection<ServiceReference<FindHook>> getBundleFindHooks() {
+ return bundleFindHooks;
+ }
+
+ public Collection<ServiceReference<org.osgi.framework.hooks.service.FindHook>> getServiceFindHooks() {
+ return serviceFindHooks;
+ }
+}
diff --git a/winegrower-core/src/main/java/org/apache/winegrower/service/OSGiServices.java b/winegrower-core/src/main/java/org/apache/winegrower/service/OSGiServices.java
index ffa4df7..da9e824 100644
--- a/winegrower-core/src/main/java/org/apache/winegrower/service/OSGiServices.java
+++ b/winegrower-core/src/main/java/org/apache/winegrower/service/OSGiServices.java
@@ -16,6 +16,7 @@
import static java.util.Arrays.asList;
import static java.util.Collections.list;
import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
@@ -23,20 +24,28 @@
import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;
import org.apache.winegrower.Ripener;
import org.apache.winegrower.api.InjectedService;
+import org.apache.winegrower.deployer.BundleContextImpl;
import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.PrototypeServiceFactory;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+import org.osgi.framework.hooks.service.ListenerHook;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationException;
@@ -55,6 +64,7 @@
private final Collection<ServiceListenerDefinition> serviceListeners = new ArrayList<>();
private final Collection<ServiceRegistrationImpl<?>> services = new ArrayList<>();
+ private final Hooks hooks = new Hooks();
private final Collection<ConfigurationListener> configurationListeners;
private final Collection<DefaultEventAdmin.EventHandlerInstance> eventListeners;
private final Ripener framework;
@@ -67,6 +77,10 @@
this.eventListeners = eventListeners;
}
+ public Hooks getHooks() {
+ return hooks;
+ }
+
public <T> T inject(final T instance) {
doInject(instance.getClass(), instance);
return instance;
@@ -117,8 +131,9 @@
.map(reg -> (T) ServiceReferenceImpl.class.cast(reg.getReference()).getReference());
}
- public synchronized void addListener(final ServiceListener listener, final Filter filter) {
- serviceListeners.add(new ServiceListenerDefinition(listener, filter));
+ public synchronized void addListener(final ServiceListener listener, final Filter filter,
+ final BundleContext context) {
+ serviceListeners.add(new ServiceListenerDefinition(listener, filter, context));
}
public synchronized void removeListener(final ServiceListener listener) {
@@ -157,10 +172,14 @@
boolean isEventHandler = Stream.of(classes).anyMatch(it -> it.equals(EventHandler.class.getName()));
final boolean removeEventHandler = isEventHandler;
+ final boolean serviceFindHook = Stream.of(classes).anyMatch(it -> it.equals(FindHook.class.getName()));
+ final boolean bundleFindHook = Stream.of(classes).anyMatch(it -> it.equals(org.osgi.framework.hooks.bundle.FindHook.class.getName()));
+ final boolean eventListenerHook = Stream.of(classes).anyMatch(it -> it.equals(EventListenerHook.class.getName()));
+ final ServiceReferenceImpl<Object> ref = new ServiceReferenceImpl<>(serviceProperties, from, service);
final ServiceRegistrationImpl<Object> registration = new ServiceRegistrationImpl<>(classes,
- serviceProperties, new ServiceReferenceImpl<>(serviceProperties, from, service), reg -> {
+ serviceProperties, ref, reg -> {
final ServiceEvent event = new ServiceEvent(ServiceEvent.UNREGISTERING, reg.getReference());
- getListeners(reg).forEach(listener -> listener.listener.serviceChanged(event));
+ fireEvent(reg, event);
synchronized (OSGiServices.this) {
services.remove(reg);
}
@@ -175,6 +194,15 @@
eventListeners.removeIf(it -> it.getHandler() == service);
}
}
+ if (serviceFindHook) {
+ hooks.getServiceFindHooks().remove(ref);
+ }
+ if (bundleFindHook) {
+ hooks.getBundleFindHooks().remove(ref);
+ }
+ if (eventListenerHook) {
+ hooks.getEventListenerHooks().remove(ref);
+ }
});
if (isEventHandler) {
@@ -187,7 +215,6 @@
null);
if (topics == null) {
LOGGER.warn("No topic for {}", service);
- isEventHandler = false;
} else {
synchronized (eventListeners) {
eventListeners.add(new DefaultEventAdmin.EventHandlerInstance(
@@ -232,7 +259,8 @@
}
services.add(registration);
- final ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, registration.getReference());
+
+ final ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, ref);
if (ManagedService.class.isInstance(service)) {
try {
ManagedService.class.cast(service).updated(serviceProperties);
@@ -240,11 +268,45 @@
throw new IllegalStateException(e);
}
}
- getListeners(registration).forEach(listener -> listener.listener.serviceChanged(event));
+ fireEvent(registration, event);
+
+ if (serviceFindHook) {
+ hooks.getServiceFindHooks().add(ServiceReference.class.cast(ref));
+ }
+ if (bundleFindHook) {
+ hooks.getBundleFindHooks().add(ServiceReference.class.cast(ref));
+ }
+ if (eventListenerHook) {
+ hooks.getEventListenerHooks().add(ServiceReference.class.cast(ref));
+ }
+
return registration;
}
- private Collection<ServiceListenerDefinition> getListeners(final ServiceRegistration<?> reg) {
+ private void fireEvent(final ServiceRegistration<?> reg, final ServiceEvent event) {
+ final List<ServiceListenerDefinition> listeners = getListeners(reg);
+ final Collection<ServiceReference<EventListenerHook>> eventListenerHooks = hooks.getEventListenerHooks();
+ if (!eventListenerHooks.isEmpty() && !listeners.isEmpty()) {
+ eventListenerHooks.forEach(hook -> {
+ final BundleContext bundleContext = hook.getBundle().getBundleContext();
+ final EventListenerHook instance = bundleContext.getService(hook);
+ if (instance != null) {
+ try {
+ final Map<BundleContext, ? extends Collection<ListenerHook.ListenerInfo>> listenerInfo = listeners.stream()
+ .collect(groupingBy(ServiceListenerDefinition::getBundleContext, toList()));
+ instance.event(event, (Map<BundleContext, Collection<ListenerHook.ListenerInfo>>) listenerInfo);
+ } catch (final Throwable th) {
+ LoggerFactory.getLogger(BundleContextImpl.class).warn("Can't call '{}'", hook, th);
+ } finally {
+ bundleContext.ungetService(hook);
+ }
+ }
+ });
+ }
+ listeners.forEach(listener -> listener.listener.serviceChanged(event));
+ }
+
+ private List<ServiceListenerDefinition> getListeners(final ServiceRegistration<?> reg) {
return serviceListeners.stream()
.filter(it -> it.filter == null || it.filter.match(reg.getReference()))
.collect(toList());
@@ -254,18 +316,36 @@
return new ArrayList<>(services);
}
- private static class ServiceListenerDefinition {
+ private static class ServiceListenerDefinition implements ListenerHook.ListenerInfo {
private final ServiceListener listener;
private final Filter filter;
+ private final BundleContext context;
- private ServiceListenerDefinition(final ServiceListener listener, final Filter filter) {
+ private ServiceListenerDefinition(final ServiceListener listener, final Filter filter,
+ final BundleContext context) {
this.listener = listener;
this.filter = filter;
+ this.context = context;
}
@Override
public String toString() {
return "ServiceListenerDefinition{listener=" + listener + ", filter=" + filter + '}';
}
+
+ @Override
+ public BundleContext getBundleContext() {
+ return context;
+ }
+
+ @Override
+ public String getFilter() {
+ return filter.toString();
+ }
+
+ @Override
+ public boolean isRemoved() {
+ return false;
+ }
}
}
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/service/HookTest.java b/winegrower-core/src/test/java/org/apache/winegrower/service/HookTest.java
new file mode 100644
index 0000000..e823d5d
--- /dev/null
+++ b/winegrower-core/src/test/java/org/apache/winegrower/service/HookTest.java
@@ -0,0 +1,63 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.winegrower.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.winegrower.Ripener;
+import org.apache.winegrower.deployer.BundleImpl;
+import org.apache.winegrower.deployer.OSGiBundleLifecycle;
+import org.apache.winegrower.test.WithRipener;
+import org.apache.winegrower.test.WithRipener.Entry;
+import org.apache.winegrower.test.WithRipener.Service;
+import org.apache.winegrower.test.hook.SimpleService;
+import org.junit.jupiter.api.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+class HookTest {
+ @Test
+ @WithRipener(includeResources = @Entry(path = "org.apache.winegrower.test.hook"))
+ void replaceServiceInstance(@Service final Ripener ripener) throws InterruptedException {
+ final BundleContext bundleContext = ripener.getRegistry().getBundles().values().stream()
+ .filter(it -> it.getBundle().getBundleId() > 0)
+ .findFirst()
+ .map(OSGiBundleLifecycle::getBundle)
+ .map(BundleImpl::getBundleContext)
+ .orElseThrow(IllegalStateException::new);
+ final ServiceTracker<SimpleService, SimpleService> tracker = new ServiceTracker<>(
+ bundleContext, SimpleService.class,
+ new ServiceTrackerCustomizer<SimpleService, SimpleService>() {
+ @Override
+ public SimpleService addingService(final ServiceReference<SimpleService> serviceReference) {
+ return serviceReference.getBundle().getBundleContext().getService(serviceReference);
+ }
+
+ @Override
+ public void modifiedService(final ServiceReference<SimpleService> serviceReference, final SimpleService simpleService) {
+ // no-op
+ }
+
+ @Override
+ public void removedService(final ServiceReference<SimpleService> serviceReference, final SimpleService simpleService) {
+ // no-op
+ }
+ });
+ tracker.open();
+ tracker.waitForService(5000L);
+ assertEquals("I am the replacement", tracker.getService().get());
+ }
+}
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/test/WithRipener.java b/winegrower-core/src/test/java/org/apache/winegrower/test/WithRipener.java
index df90d71..d8dc144 100644
--- a/winegrower-core/src/test/java/org/apache/winegrower/test/WithRipener.java
+++ b/winegrower-core/src/test/java/org/apache/winegrower/test/WithRipener.java
@@ -227,7 +227,7 @@
@Override
public boolean supportsParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext)
throws ParameterResolutionException {
- return supports(parameterContext.getParameter().getType());
+ return supports(parameterContext.getParameter().getType()) || parameterContext.getParameter().isAnnotationPresent(Service.class);
}
@Override
@@ -241,7 +241,8 @@
}
private <T> T findInjection(final ExtensionContext extensionContext, final Class<T> type) {
- return extensionContext.getStore(NAMESPACE).get(type, type);
+ return ofNullable(extensionContext.getStore(NAMESPACE).get(type, type))
+ .orElseGet(() -> findInjection(extensionContext, Ripener.class).getServices().findService(type).orElse(null));
}
private static class Context implements AutoCloseable {
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/test/hook/RegisterHooks.java b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/RegisterHooks.java
new file mode 100644
index 0000000..c23e957
--- /dev/null
+++ b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/RegisterHooks.java
@@ -0,0 +1,47 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.winegrower.test.hook;
+
+import static org.osgi.framework.Constants.BUNDLE_ACTIVATOR;
+
+import java.util.Hashtable;
+
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+
+@Header(name = BUNDLE_ACTIVATOR, value = "${@class}")
+public class RegisterHooks implements BundleActivator {
+ @Override
+ public void start(final BundleContext context) {
+ // in practise we would get all services to replace existing one too,
+ // here we control what we want to replace (replaced=true) so let's keep it simple for tests
+ final ServiceReplacer replacer = new ServiceReplacer(context.getBundle().getBundleId());
+ context.addServiceListener(replacer);
+ context.registerService(
+ new String[] { FindHook.class.getName(), EventListenerHook.class.getName() },
+ replacer, new Hashtable<>());
+
+ context.registerService(SimpleService.class, new SimpleService(), new Hashtable<String, Object>() {{
+ put("replaced", "true");
+ }});
+ }
+
+ @Override
+ public void stop(final BundleContext context) {
+ // no-op
+ }
+}
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/test/hook/ServiceReplacer.java b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/ServiceReplacer.java
new file mode 100644
index 0000000..abb8bf8
--- /dev/null
+++ b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/ServiceReplacer.java
@@ -0,0 +1,65 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.winegrower.test.hook;
+
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+import org.osgi.framework.hooks.service.ListenerHook;
+
+public class ServiceReplacer implements FindHook, EventListenerHook, ServiceListener {
+ private final long bundleId;
+
+ public ServiceReplacer(final long bundleId) {
+ this.bundleId = bundleId;
+ }
+
+ @Override // replaced services are not forward to listeners except the bundle owning the replacer and #0 (optional for the test)
+ public void event(final ServiceEvent event, final Map<BundleContext, Collection<ListenerHook.ListenerInfo>> listeners) {
+ if (event.getServiceReference().getProperty("replaced") != null) {
+ listeners.keySet().removeIf(b -> b.getBundle().getBundleId() != 0);
+ }
+ }
+
+ @Override // remove replaced services to keep only replacements
+ public void find(final BundleContext context, final String name, final String filter,
+ final boolean allServices, final Collection<ServiceReference<?>> references) {
+ final long consumingBundleId = context.getBundle().getBundleId();
+ if (consumingBundleId != 0) {
+ references.removeIf(r -> r.getProperty("replaced") != null);
+ }
+ }
+
+ @Override // actual replacement
+ public void serviceChanged(final ServiceEvent serviceEvent) {
+ if (serviceEvent.getServiceReference().getProperty("replaced") != null) {
+ final BundleContext context = serviceEvent.getServiceReference().getBundle().getBundleContext();
+ final String clazz = String.valueOf(serviceEvent.getServiceReference().getProperty(Constants.OBJECTCLASS));
+ context.registerService(clazz, new SimpleService() {
+ @Override
+ public String get() {
+ return "I am the replacement";
+ }
+ }, new Hashtable<>());
+ }
+ }
+}
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/test/hook/SimpleHook.java b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/SimpleHook.java
new file mode 100644
index 0000000..4f6603c
--- /dev/null
+++ b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/SimpleHook.java
@@ -0,0 +1,4 @@
+package org.apache.winegrower.test.hook;
+
+public class SimpleHook {
+}
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/test/hook/SimpleService.java b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/SimpleService.java
new file mode 100644
index 0000000..a8a1068
--- /dev/null
+++ b/winegrower-core/src/test/java/org/apache/winegrower/test/hook/SimpleService.java
@@ -0,0 +1,20 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.winegrower.test.hook;
+
+public class SimpleService {
+ public String get() {
+ return "simple";
+ }
+}