Merge pull request #26 from rmannibucau/rmannibucau/KARAF-6899

[KARAF-6899] add LifecycleCallbacks API
diff --git a/winegrower-cepages/winegrower-cepage-osgi-cdi/pom.xml b/winegrower-cepages/winegrower-cepage-osgi-cdi/pom.xml
index e44545b..e72018e 100644
--- a/winegrower-cepages/winegrower-cepage-osgi-cdi/pom.xml
+++ b/winegrower-cepages/winegrower-cepage-osgi-cdi/pom.xml
@@ -79,6 +79,7 @@
       <groupId>org.apache.aries.spifly</groupId>
       <artifactId>org.apache.aries.spifly.dynamic.bundle</artifactId>
       <version>1.3.2</version>
+      <optional>true</optional> <!-- not really needed -->
     </dependency>
     <dependency>
       <groupId>org.apache.geronimo.specs</groupId>
diff --git a/winegrower-core/src/main/java/org/apache/winegrower/Ripener.java b/winegrower-core/src/main/java/org/apache/winegrower/Ripener.java
index 50c0918..c03e8f9 100644
--- a/winegrower-core/src/main/java/org/apache/winegrower/Ripener.java
+++ b/winegrower-core/src/main/java/org/apache/winegrower/Ripener.java
@@ -13,6 +13,7 @@
  */
 package org.apache.winegrower;
 
+import org.apache.winegrower.api.LifecycleCallbacks;
 import org.apache.winegrower.deployer.OSGiBundleLifecycle;
 import org.apache.winegrower.scanner.StandaloneScanner;
 import org.apache.winegrower.scanner.manifest.HeaderManifestContributor;
@@ -60,6 +61,8 @@
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.logging.Level;
 import java.util.stream.Stream;
@@ -67,6 +70,7 @@
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
+import static java.util.Comparator.comparing;
 import static java.util.Locale.ROOT;
 import static java.util.Optional.ofNullable;
 import static java.util.function.Function.identity;
@@ -141,6 +145,29 @@
                 "org.apache.aries.cdi");
         private List<String> defaultConfigurationAdminPids;
 
+        private List<LifecycleCallbacks> lifecycleCallbacks;
+
+        /**
+         * Only for SPI ones, the epxlicit ones in {@link #lifecycleCallbacks} are always used.
+         */
+        private boolean useLifecycleCallbacks = true;
+
+        public boolean isUseLifecycleCallbacks() {
+            return useLifecycleCallbacks;
+        }
+
+        public void setUseLifecycleCallbacks(final boolean useLifecycleCallbacks) {
+            this.useLifecycleCallbacks = useLifecycleCallbacks;
+        }
+
+        public List<LifecycleCallbacks> getLifecycleCallbacks() {
+            return lifecycleCallbacks;
+        }
+
+        public void setLifecycleCallbacks(final List<LifecycleCallbacks> lifecycleCallbacks) {
+            this.lifecycleCallbacks = lifecycleCallbacks;
+        }
+
         public List<String> getDefaultConfigurationAdminPids() {
             return defaultConfigurationAdminPids;
         }
@@ -293,13 +320,31 @@
         public Impl(final Configuration configuration) {
             this.configuration = configuration;
 
+            final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+            if (configuration.isUseLifecycleCallbacks()) {
+                final List<LifecycleCallbacks> callbacks = StreamSupport.stream(
+                        ServiceLoader.load(LifecycleCallbacks.class, contextClassLoader).spliterator(), false)
+                        .sorted(comparing(LifecycleCallbacks::order))
+                        .collect(toList());
+                if (configuration.getLifecycleCallbacks() == null) {
+                    configuration.setLifecycleCallbacks(callbacks);
+                } else {
+                    configuration.setLifecycleCallbacks(Stream.concat(
+                            configuration.getLifecycleCallbacks().stream(),
+                            callbacks.stream()).sorted(comparing(LifecycleCallbacks::order))
+                            .collect(toList()));
+                }
+            } else if (configuration.getLifecycleCallbacks() == null) {
+                configuration.setLifecycleCallbacks(emptyList());
+            }
+            runCallbacks(LifecycleCallbacks::processConfiguration, configuration);
+
             final Collection<ConfigurationListener> configurationListeners = new ArrayList<>();
             final Collection<DefaultEventAdmin.EventHandlerInstance> eventListeners = new ArrayList<>();
             this.services = new OSGiServices(this, configurationListeners, eventListeners);
             this.registry = new BundleRegistry(services, configuration);
 
-            try (final InputStream stream = Thread.currentThread().getContextClassLoader()
-                    .getResourceAsStream("winegrower.properties")) {
+            try (final InputStream stream = contextClassLoader.getResourceAsStream("winegrower.properties")) {
                 loadConfiguration(stream);
             } catch (final IOException e) {
                 LOGGER.warn(e.getMessage());
@@ -322,6 +367,10 @@
             this.services.registerService(new String[]{type.getName()}, impl, props, this.registry.getBundles().get(0L).getBundle());
         }
 
+        private <A> void runCallbacks(final BiConsumer<LifecycleCallbacks, A> action, final A arg) {
+            configuration.getLifecycleCallbacks().forEach(c -> action.accept(c, arg));
+        }
+
         private ConfigurationAdmin loadConfigurationAdmin(final Collection<ConfigurationListener> configurationListeners) {
             final Iterator<ConfigurationAdmin> configurationAdminIterator = ServiceLoader.load(ConfigurationAdmin.class).iterator();
             if (configurationAdminIterator.hasNext()) {
@@ -450,28 +499,33 @@
 
         @Override
         public synchronized Ripener start() {
-            startTime = System.currentTimeMillis();
-            LOGGER.info("Starting Apache Winegrower application on {}",
-                    LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneId.systemDefault()));
-            if (configuration.isLazyInstall()) {
-                return this;
+            runCallbacks(LifecycleCallbacks::beforeStart, this);
+            try {
+                startTime = System.currentTimeMillis();
+                LOGGER.info("Starting Apache Winegrower application on {}",
+                        LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneId.systemDefault()));
+                if (configuration.isLazyInstall()) {
+                    return this;
+                }
+                final StandaloneScanner scanner = getScanner();
+                final AtomicLong bundleIdGenerator = new AtomicLong(1);
+                Stream.concat(Stream.concat(
+                        scanner.findOSGiBundles().stream(),
+                        scanner.findPotentialOSGiBundles().stream()),
+                        scanner.findEmbeddedClasses().stream())
+                        .sorted(this::compareBundles)
+                        .map(it -> new OSGiBundleLifecycle(
+                                it.getManifest(), it.getJar(),
+                                services, registry, configuration,
+                                bundleIdGenerator.getAndIncrement(),
+                                it.getFiles()))
+                        .peek(OSGiBundleLifecycle::start)
+                        .peek(it -> registry.getBundles().put(it.getBundle().getBundleId(), it))
+                        .forEach(bundle -> LOGGER.debug("Bundle {}", bundle));
+                this.scanner = null; // we don't need it anymore since we don't support runtime install so make it gc friendly
+            } finally {
+                runCallbacks(LifecycleCallbacks::afterStart, this);
             }
-            final StandaloneScanner scanner = getScanner();
-            final AtomicLong bundleIdGenerator = new AtomicLong(1);
-            Stream.concat(Stream.concat(
-                    scanner.findOSGiBundles().stream(),
-                    scanner.findPotentialOSGiBundles().stream()),
-                    scanner.findEmbeddedClasses().stream())
-                    .sorted(this::compareBundles)
-                    .map(it -> new OSGiBundleLifecycle(
-                            it.getManifest(), it.getJar(),
-                            services, registry, configuration,
-                            bundleIdGenerator.getAndIncrement(),
-                            it.getFiles()))
-                    .peek(OSGiBundleLifecycle::start)
-                    .peek(it -> registry.getBundles().put(it.getBundle().getBundleId(), it))
-                    .forEach(bundle -> LOGGER.debug("Bundle {}", bundle));
-            this.scanner = null; // we don't need it anymore since we don't support runtime install so make it gc friendly
             return this;
         }
 
@@ -481,33 +535,38 @@
 
         @Override
         public synchronized void stop() {
-            LOGGER.info("Stopping Apache Winegrower application on {}", LocalDateTime.now());
-            final Map<Long, OSGiBundleLifecycle> bundles = registry.getBundles();
-            bundles.values().stream()
-                    .sorted((o1, o2) -> (int) (o2.getBundle().getBundleId() - o1.getBundle().getBundleId()))
-                    .forEach(OSGiBundleLifecycle::stop);
-            bundles.clear();
-            if (configuration.getWorkDir().exists()) {
-                try {
-                    Files.walkFileTree(configuration.getWorkDir().toPath(), new SimpleFileVisitor<Path>() {
-                        @Override
-                        public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
-                            Files.delete(file);
-                            return super.visitFile(file, attrs);
-                        }
+            runCallbacks(LifecycleCallbacks::beforeStop, this);
+            try {
+                LOGGER.info("Stopping Apache Winegrower application on {}", LocalDateTime.now());
+                final Map<Long, OSGiBundleLifecycle> bundles = registry.getBundles();
+                bundles.values().stream()
+                        .sorted((o1, o2) -> (int) (o2.getBundle().getBundleId() - o1.getBundle().getBundleId()))
+                        .forEach(OSGiBundleLifecycle::stop);
+                bundles.clear();
+                if (configuration.getWorkDir().exists()) {
+                    try {
+                        Files.walkFileTree(configuration.getWorkDir().toPath(), new SimpleFileVisitor<Path>() {
+                            @Override
+                            public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+                                Files.delete(file);
+                                return super.visitFile(file, attrs);
+                            }
 
-                        @Override
-                        public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
-                            Files.delete(dir);
-                            return super.postVisitDirectory(dir, exc);
-                        }
-                    });
-                } catch (final IOException e) {
-                    LOGGER.warn("Can't delete work directory", e);
+                            @Override
+                            public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
+                                Files.delete(dir);
+                                return super.postVisitDirectory(dir, exc);
+                            }
+                        });
+                    } catch (final IOException e) {
+                        LOGGER.warn("Can't delete work directory", e);
+                    }
                 }
-            }
-            if (DefaultEventAdmin.class.isInstance(eventAdmin)) {
-                DefaultEventAdmin.class.cast(eventAdmin).close();
+                if (DefaultEventAdmin.class.isInstance(eventAdmin)) {
+                    DefaultEventAdmin.class.cast(eventAdmin).close();
+                }
+            } finally {
+                runCallbacks(LifecycleCallbacks::afterStop, this);
             }
         }
 
diff --git a/winegrower-core/src/main/java/org/apache/winegrower/api/LifecycleCallbacks.java b/winegrower-core/src/main/java/org/apache/winegrower/api/LifecycleCallbacks.java
new file mode 100644
index 0000000..31ee1b6
--- /dev/null
+++ b/winegrower-core/src/main/java/org/apache/winegrower/api/LifecycleCallbacks.java
@@ -0,0 +1,51 @@
+/**
+ * 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.api;
+
+import org.apache.winegrower.Ripener;
+
+/**
+ * Enables to interact with Ripener before/after it is active.
+ * Very convenient to register custom built in services for example.
+ * It is registered as a plain java SPI (META-INF/services/org.apache.winegrower.api.LifecycleCallbacks).
+ */
+public interface LifecycleCallbacks {
+    /**
+     * @return callbacks are sorted thanks to this order (natural int order).
+     */
+    default int order() {
+        return 1000;
+    }
+
+    // called before ripener is setup
+    default void processConfiguration(final Ripener.Configuration configuration) {
+        // no-op
+    }
+
+    default void beforeStart(final Ripener ripener) {
+        // no-op
+    }
+
+    default void afterStart(final Ripener ripener) {
+        // no-op
+    }
+
+    default void beforeStop(final Ripener ripener) {
+        // no-op
+    }
+
+    default void afterStop(final Ripener ripener) {
+        // no-op
+    }
+}
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/api/LifecycleCallbacksTest.java b/winegrower-core/src/test/java/org/apache/winegrower/api/LifecycleCallbacksTest.java
new file mode 100644
index 0000000..70e6242
--- /dev/null
+++ b/winegrower-core/src/test/java/org/apache/winegrower/api/LifecycleCallbacksTest.java
@@ -0,0 +1,58 @@
+package org.apache.winegrower.api;
+
+import org.apache.winegrower.Ripener;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class LifecycleCallbacksTest {
+    @Test
+    void lifecycle() {
+        final MyCallback callback = new MyCallback();
+        final Ripener.Configuration configuration = new Ripener.Configuration();
+        configuration.setLifecycleCallbacks(singletonList(callback));
+        System.setProperty("winegrower.scanner.standalone.skipUrlsScanning", "true");
+        try (final Ripener ripener = Ripener.create(configuration).start()) {
+            // no-op
+        } finally {
+            System.clearProperty("winegrower.scanner.standalone.skipUrlsScanning");
+        }
+        assertEquals(
+                asList("processConfiguration=true", "beforeStart=true", "afterStart=true", "beforeStop=true", "afterStop=true"),
+                callback.events);
+    }
+
+    public static class MyCallback implements LifecycleCallbacks {
+        private final List<String> events = new ArrayList<>();
+
+        @Override
+        public void processConfiguration(final Ripener.Configuration configuration) {
+            events.add("processConfiguration=" + (configuration != null));
+        }
+
+        @Override
+        public void beforeStart(final Ripener ripener) {
+            events.add("beforeStart=" + (ripener != null));
+        }
+
+        @Override
+        public void afterStart(final Ripener ripener) {
+            events.add("afterStart=" + (ripener != null));
+        }
+
+        @Override
+        public void beforeStop(final Ripener ripener) {
+            events.add("beforeStop=" + (ripener != null));
+        }
+
+        @Override
+        public void afterStop(final Ripener ripener) {
+            events.add("afterStop=" + (ripener != null));
+        }
+    }
+}
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 51046ea..ea1baf1 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
@@ -20,12 +20,14 @@
 import static java.util.Collections.singletonList;
 import static java.util.Optional.of;
 import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.toList;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
+import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -45,6 +47,7 @@
 import java.util.stream.Stream;
 
 import org.apache.winegrower.Ripener;
+import org.apache.winegrower.api.LifecycleCallbacks;
 import org.junit.jupiter.api.extension.AfterEachCallback;
 import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -81,6 +84,8 @@
 
     Entry[] includeResources() default {};
 
+    boolean addLifecycleCallbackSpy() default false;
+
     class Extension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
 
         private static final String CLASSES_BASE = System.getProperty(Extension.class.getName() + ".classesBase",
@@ -103,7 +108,7 @@
 
             final Ripener.Configuration configuration = new Ripener.Configuration();
             configuration.setScanningExcludes(asList("common-java5-" /* surefire, yes... */, "test-classes"));
-            setConfiguration(configuration, config);
+            setConfiguration(configuration, config, extensionContext.getTestClass().orElseThrow(IllegalStateException::new));
 
             final Ripener ripener = new Ripener.Impl(configuration).start();
             store.put(Ripener.class, ripener);
@@ -126,7 +131,8 @@
             ofNullable(store.get(Ripener.class, Ripener.class)).ifPresent(Ripener::stop);
         }
 
-        private void setConfiguration(final Ripener.Configuration configuration, final WithRipener config) {
+        private void setConfiguration(final Ripener.Configuration configuration, final WithRipener config,
+                                      final Class<?> test) {
             final Collection<String> includes = asList(config.includes());
             if (!includes.isEmpty()) {
                 configuration.setJarFilter(it -> includes.stream().anyMatch(e -> e.startsWith(it)));
@@ -136,6 +142,19 @@
             if (!workDir.isEmpty()) {
                 configuration.setWorkDir(new File(workDir));
             }
+
+            if (config.addLifecycleCallbackSpy()) {
+                configuration.setLifecycleCallbacks(Stream.of(test.getClasses())
+                        .filter(LifecycleCallbacks.class::isAssignableFrom)
+                        .map(it -> {
+                            try {
+                                return it.asSubclass(LifecycleCallbacks.class).getConstructor().newInstance();
+                            } catch (final InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+                                throw new IllegalStateException(e);
+                            }
+                        })
+                        .collect(toList()));
+            }
         }
 
         private URL[] createUrls(final WithRipener config, final ExtensionContext context) {
diff --git a/winegrower-extension/winegrower-agent/src/main/java/org/apache/winegrower/extension/agent/WinegrowerAgent.java b/winegrower-extension/winegrower-agent/src/main/java/org/apache/winegrower/extension/agent/WinegrowerAgent.java
index d198746..d53f0c6 100644
--- a/winegrower-extension/winegrower-agent/src/main/java/org/apache/winegrower/extension/agent/WinegrowerAgent.java
+++ b/winegrower-extension/winegrower-agent/src/main/java/org/apache/winegrower/extension/agent/WinegrowerAgent.java
@@ -170,10 +170,43 @@
 
     private static Object createConfiguration(final Class<?> configType, final String agentArgs) throws Throwable {
         final Object configuration = configType.getConstructor().newInstance();
+        final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
         ofNullable(extractConfig(agentArgs,"workDir="))
                 .map(String::valueOf)
                 .map(File::new)
                 .ifPresent(value -> doCall(configuration, "setWorkDir", new Class<?>[]{File.class}, new Object[]{value}));
+        ofNullable(extractConfig(agentArgs,"useLifecycleCallbacks="))
+                .map(String::valueOf)
+                .map(Boolean::parseBoolean)
+                .ifPresent(value -> doCall(configuration, "setUseLifecycleCallbacks", new Class<?>[]{boolean.class}, new Object[]{value}));
+        ofNullable(extractConfig(agentArgs,"lifecycleCallbacks="))
+                .map(String::valueOf)
+                .filter(it -> !it.isEmpty())
+                .map(it -> asList(it.split(",")))
+                .map(callbacks -> {
+                    try {
+                        final Class<?> type = contextClassLoader.loadClass("org.apache.winegrower.api.LifecycleCallbacks");
+                        return callbacks.stream()
+                                .map(clazz -> {
+                                    try {
+                                        return contextClassLoader
+                                                .loadClass(clazz)
+                                                .getConstructor()
+                                                .newInstance();
+                                    } catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
+                                        throw new IllegalArgumentException(e);
+                                    } catch (final InvocationTargetException e) {
+                                        throw new IllegalArgumentException(
+                                                e.getTargetException());
+                                    }
+                                })
+                                .map(type::cast)
+                                .collect(toList());
+                    } catch (final ClassNotFoundException e) {
+                        throw new IllegalArgumentException(e);
+                    }
+                })
+                .ifPresent(value -> doCall(configuration, "setLifecycleCallbacks", new Class<?>[]{List.class}, new Object[]{value}));
         ofNullable(extractConfig(agentArgs,"prioritizedBundles="))
                 .map(String::valueOf)
                 .filter(it -> !it.isEmpty())
@@ -200,13 +233,12 @@
                 .map(it -> asList(it.split(",")))
                 .ifPresent(contributors -> {
                     try {
-                        final Class<?> type = Thread.currentThread().getContextClassLoader().loadClass(
+                        final Class<?> type = contextClassLoader.loadClass(
                                     "org.apache.winegrower.scanner.manifest.ManifestContributor");
                         final Collection<?> value = contributors.stream()
                                                       .map(clazz -> {
                                                           try {
-                                                              return Thread.currentThread()
-                                                                           .getContextClassLoader()
+                                                              return contextClassLoader
                                                                            .loadClass(clazz)
                                                                            .getConstructor()
                                                                            .newInstance();
@@ -229,8 +261,7 @@
                 .filter(it -> !it.isEmpty())
                 .ifPresent(filter -> {
                     try {
-                        final Predicate<String> predicate = (Predicate<String>) Thread.currentThread()
-                              .getContextClassLoader().loadClass(filter).getConstructor().newInstance();
+                        final Predicate<String> predicate = (Predicate<String>) contextClassLoader.loadClass(filter).getConstructor().newInstance();
                         doCall(configuration, "setJarFilter", new Class<?>[]{Predicate.class}, new Object[]{predicate});
                     } catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
                         throw new IllegalArgumentException(e);
diff --git a/winegrower-extension/winegrower-build/winegrower-maven-plugin/src/main/java/org/apache/winegrower/extension/build/maven/PourMojo.java b/winegrower-extension/winegrower-build/winegrower-maven-plugin/src/main/java/org/apache/winegrower/extension/build/maven/PourMojo.java
index 182b86e..d91eb46 100644
--- a/winegrower-extension/winegrower-build/winegrower-maven-plugin/src/main/java/org/apache/winegrower/extension/build/maven/PourMojo.java
+++ b/winegrower-extension/winegrower-build/winegrower-maven-plugin/src/main/java/org/apache/winegrower/extension/build/maven/PourMojo.java
@@ -18,6 +18,7 @@
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.project.MavenProject;
+import org.apache.winegrower.api.LifecycleCallbacks;
 import org.apache.xbean.finder.util.Files;
 
 import java.io.File;
@@ -65,6 +66,12 @@
     @Parameter(property = "winegrower.prioritizedBundles")
     private List<String> prioritizedBundles;
 
+    @Parameter(property = "winegrower.lifecycleCallbacks")
+    private List<Class<?>> lifecycleCallbacks;
+
+    @Parameter(property = "winegrower.useLifecycleCallbacks", defaultValue = "true")
+    private boolean useLifecycleCallbacks;
+
     @Parameter(property = "winegrower.systemVariables")
     private Map<String, String> systemVariables;
 
@@ -158,6 +165,22 @@
     private Object createConfiguration(final Class<?> configClass)
             throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
         final Object configuration = configClass.getConstructor().newInstance();
+        doCall(configuration, "setUseLifecycleCallbacks", new Class<?>[]{boolean.class}, new Object[]{useLifecycleCallbacks});
+        ofNullable(lifecycleCallbacks).map(it -> it.stream()
+                .map(clazz -> {
+                    try {
+                        return clazz
+                                .getConstructor()
+                                .newInstance();
+                    } catch (final InstantiationException | NoSuchMethodException | IllegalAccessException e) {
+                        throw new IllegalArgumentException(e);
+                    } catch (final InvocationTargetException e) {
+                        throw new IllegalArgumentException(
+                                e.getTargetException());
+                    }
+                })
+                .collect(toList()))
+                .ifPresent(value -> doCall(configuration, "setLifecycleCallbacks", new Class<?>[]{List.class}, new Object[]{value}));
         ofNullable(workDir)
                 .ifPresent(value -> doCall(configuration, "setWorkDir", new Class<?>[]{File.class}, new Object[]{value}));
         ofNullable(prioritizedBundles)
diff --git a/winegrower-extension/winegrower-servlet/src/main/java/org/apache/winegrower/servlet/service/ServletHttpServiceDeployer.java b/winegrower-extension/winegrower-servlet/src/main/java/org/apache/winegrower/servlet/service/ServletHttpServiceDeployer.java
index f57f5ab..1507235 100644
--- a/winegrower-extension/winegrower-servlet/src/main/java/org/apache/winegrower/servlet/service/ServletHttpServiceDeployer.java
+++ b/winegrower-extension/winegrower-servlet/src/main/java/org/apache/winegrower/servlet/service/ServletHttpServiceDeployer.java
@@ -14,6 +14,7 @@
 package org.apache.winegrower.servlet.service;
 
 import org.apache.winegrower.Ripener;
+import org.apache.winegrower.api.LifecycleCallbacks;
 import org.apache.winegrower.scanner.manifest.ManifestContributor;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
@@ -181,6 +182,10 @@
 
     private Ripener.Configuration createConfiguration(final ServletContext servletContext) {
         final Ripener.Configuration configuration = new Ripener.Configuration();
+        ofNullable(servletContext.getInitParameter("winegrower.servlet.ripener.configuration.useLifecycleCallbacks"))
+                .map(String::valueOf)
+                .map(Boolean::parseBoolean)
+                .ifPresent(configuration::setUseLifecycleCallbacks);
         ofNullable(servletContext.getInitParameter("winegrower.servlet.ripener.configuration.workdir")).map(String::valueOf)
                 .map(File::new).ifPresent(configuration::setWorkDir);
         ofNullable(servletContext.getInitParameter("winegrower.servlet.ripener.configuration.prioritizedBundles"))
@@ -208,6 +213,19 @@
                 }
             }).map(ManifestContributor.class::cast).collect(toList()));
         });
+        ofNullable(servletContext.getInitParameter("winegrower.servlet.ripener.configuration.lifecycleCallbacks"))
+                .map(String::valueOf).filter(it -> !it.isEmpty()).map(it -> asList(it.split(","))).ifPresent(lifecycleCallbacks -> {
+            configuration.setLifecycleCallbacks(lifecycleCallbacks.stream().map(clazz -> {
+                try {
+                    return Thread.currentThread().getContextClassLoader().loadClass(clazz).getConstructor().newInstance();
+                } catch (final InstantiationException | NoSuchMethodException | IllegalAccessException
+                        | ClassNotFoundException e) {
+                    throw new IllegalArgumentException(e);
+                } catch (final InvocationTargetException e) {
+                    throw new IllegalArgumentException(e.getTargetException());
+                }
+            }).map(LifecycleCallbacks.class::cast).collect(toList()));
+        });
         ofNullable(servletContext.getInitParameter("winegrower.servlet.ripener.configuration.jarFilter")).map(String::valueOf)
                 .filter(it -> !it.isEmpty()).ifPresent(filter -> {
             try {
diff --git a/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/Winegrower.java b/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/Winegrower.java
index 216aba0..fa676ef 100644
--- a/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/Winegrower.java
+++ b/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/Winegrower.java
@@ -20,6 +20,7 @@
 import java.lang.annotation.Target;
 import java.util.function.Predicate;
 
+import org.apache.winegrower.api.LifecycleCallbacks;
 import org.apache.winegrower.extension.testing.junit5.internal.WinegrowerExtension;
 import org.apache.winegrower.scanner.manifest.ManifestContributor;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -38,6 +39,8 @@
     String[] ignoredBundles() default {};
     String[] scanningExcludes() default {};
     String[] scanningIncludes() default {};
+    Class<? extends LifecycleCallbacks>[] lifecycleCallbacks() default {};
+    boolean useLifecycleCallbacks() default true;
 
     interface JarFilter extends Predicate<String> {
     }
diff --git a/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/internal/WinegrowerExtension.java b/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/internal/WinegrowerExtension.java
index 7006097..8b78b3c 100644
--- a/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/internal/WinegrowerExtension.java
+++ b/winegrower-extension/winegrower-testing/winegrower-testing-junit5/src/main/java/org/apache/winegrower/extension/testing/junit5/internal/WinegrowerExtension.java
@@ -48,6 +48,8 @@
 
     private Ripener.Configuration createConfiguration(final Winegrower winegrower) {
         final Ripener.Configuration configuration = new Ripener.Configuration();
+        of(winegrower.useLifecycleCallbacks())
+                .ifPresent(configuration::setUseLifecycleCallbacks);
         of(winegrower.workDir())
                 .filter(it -> !it.isEmpty())
                 .ifPresent(wd -> configuration.setWorkDir(new File(wd)));
@@ -85,6 +87,17 @@
                         throw new IllegalArgumentException(e.getTargetException());
                     }
                 }).collect(toList())));
+        of(winegrower.lifecycleCallbacks())
+                .filter(it -> it.length > 0)
+                .ifPresent(value -> configuration.setLifecycleCallbacks(Stream.of(value).map(it -> {
+                    try {
+                        return it.getConstructor().newInstance();
+                    } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException e) {
+                        throw new IllegalArgumentException(e);
+                    } catch (final InvocationTargetException e) {
+                        throw new IllegalArgumentException(e.getTargetException());
+                    }
+                }).collect(toList())));
         return configuration;
     }