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";
+    }
+}