You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by jb...@apache.org on 2020/10/20 06:12:51 UTC

[karaf-winegrower] branch master updated: basic winegrower osgi framework impl - still some enhancements but sh… (#12)

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

jbonofre pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/karaf-winegrower.git


The following commit(s) were added to refs/heads/master by this push:
     new e4e375a  basic winegrower osgi framework impl - still some enhancements but sh… (#12)
e4e375a is described below

commit e4e375a719657202600d2ff8542212abadc94d37
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Tue Oct 20 08:06:05 2020 +0200

    basic winegrower osgi framework impl - still some enhancements but sh… (#12)
    
    * basic winegrower osgi framework impl - still some enhancements but should enable to use it with karaf main/wrapper
    
    * adding framework factory
    
    Co-authored-by: Jean-Baptiste Onofré <jb...@apache.org>
---
 winegrower-core/pom.xml                            |  48 ++-
 winegrower-core/src/main/asciidoc/index.adoc       |  15 +
 .../main/java/org/apache/winegrower/Ripener.java   | 137 ++++----
 .../winegrower/deployer/BundleContextImpl.java     |  12 +-
 .../winegrower/framework/WinegrowerFramework.java  | 356 +++++++++++++++++++++
 .../winegrower/scanner/StandaloneScanner.java      |  20 +-
 .../framework/WinegrowerFrameworkTest.java         |  46 +++
 7 files changed, 574 insertions(+), 60 deletions(-)

diff --git a/winegrower-core/pom.xml b/winegrower-core/pom.xml
index 88f5124..4ca60c1 100644
--- a/winegrower-core/pom.xml
+++ b/winegrower-core/pom.xml
@@ -15,7 +15,7 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://maven.apache.org/POM/4.0.0             http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
   <modelVersion>4.0.0</modelVersion>
 
@@ -69,4 +69,50 @@
       <scope>test</scope>
     </dependency>
   </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>3.2.4</version>
+        <executions>
+          <execution>
+            <id>fatjar</id>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <shadedArtifactAttached>true</shadedArtifactAttached>
+              <shadedClassifierName>fatjar</shadedClassifierName>
+              <createDependencyReducedPom>false</createDependencyReducedPom>
+              <dependencyReducedPomLocation>${project.build.directory}/shade.pom</dependencyReducedPomLocation>
+              <artifactSet>
+                <includes>
+                  <include>org.apache.xbean:*</include>
+                </includes>
+              </artifactSet>
+              <filters>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                  </excludes>
+                </filter>
+                <filter>
+                  <artifact>org.apache.xbean:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/MANIFEST.MF</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/winegrower-core/src/main/asciidoc/index.adoc b/winegrower-core/src/main/asciidoc/index.adoc
index 156747c..7ce457b 100644
--- a/winegrower-core/src/main/asciidoc/index.adoc
+++ b/winegrower-core/src/main/asciidoc/index.adoc
@@ -29,3 +29,18 @@ This is done providing a `winegrower.properties` in the agent classloader. It ta
 
 TIP: you can also pass `winegrower.service.*` properties in this file.
 
+=== Use as Karaf framework
+
+In some cloud oriented application it can makes sense to deploy Winegrower as a Karaf framework.
+To do so:
+
+1. Customize your `config.properties` setting/adding these properties:
++
+[source,properties]
+----
+karaf.framework=winegrower
+karaf.framework.factory=org.apache.winegrower.framework.WinegrowerFramework$Factory
+karaf.framework.winegrower=mvn\:org.apache.winegrower/winegrower-core/<version>/jar/fatjar
+----
+2. Ensure `winegrower-core` fatjar is in `system/` directly of Karaf
+3. Ensure `winegrower-core` OSGi API dependencies are in `karaf/lib` (typically config admin is required or `osgi.cmpn`)
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 fdbeb38..69b34b2 100644
--- a/winegrower-core/src/main/java/org/apache/winegrower/Ripener.java
+++ b/winegrower-core/src/main/java/org/apache/winegrower/Ripener.java
@@ -115,6 +115,7 @@ public interface Ripener extends AutoCloseable {
 
         private File workDir = new File(System.getProperty("java.io.tmpdir"), "karaf-boot_" + UUID.randomUUID().toString());
         private Predicate<String> jarFilter = it -> DEFAULT_EXCLUSIONS.stream().anyMatch(it::startsWith);
+        private boolean lazyInstall;
         private Collection<String> scanningIncludes;
         private Collection<String> scanningExcludes;
         private Collection<String> ignoredBundles = emptyList();
@@ -138,6 +139,14 @@ public interface Ripener extends AutoCloseable {
                 "pax-web-runtime",
                 "org.apache.aries.cdi");
 
+        public boolean isLazyInstall() {
+            return lazyInstall;
+        }
+
+        public void setLazyInstall(boolean lazyInstall) {
+            this.lazyInstall = lazyInstall;
+        }
+
         public Collection<String> getIgnoredBundles() {
             return ignoredBundles;
         }
@@ -193,8 +202,65 @@ public interface Ripener extends AutoCloseable {
         public Predicate<String> getJarFilter() {
             return jarFilter;
         }
-    }
 
+        public void fromProperties(final Properties properties) {
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.workdir"))
+                    .map(String::valueOf)
+                    .map(File::new)
+                    .ifPresent(this::setWorkDir);
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.lazyInstall"))
+                    .map(String::valueOf)
+                    .map(Boolean::parseBoolean)
+                    .ifPresent(this::setLazyInstall);
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.prioritizedBundles"))
+                    .map(String::valueOf)
+                    .filter(it -> !it.isEmpty())
+                    .map(it -> asList(it.split(",")))
+                    .ifPresent(this::setPrioritizedBundles);
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.ignoredBundles"))
+                    .map(String::valueOf)
+                    .filter(it -> !it.isEmpty())
+                    .map(it -> asList(it.split(",")))
+                    .ifPresent(this::setIgnoredBundles);
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.scanningIncludes"))
+                    .map(String::valueOf)
+                    .filter(it -> !it.isEmpty())
+                    .map(it -> asList(it.split(",")))
+                    .ifPresent(this::setScanningIncludes);
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.scanningExcludes"))
+                    .map(String::valueOf)
+                    .filter(it -> !it.isEmpty())
+                    .map(it -> asList(it.split(",")))
+                    .ifPresent(this::setScanningExcludes);
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.manifestContributors"))
+                    .map(String::valueOf)
+                    .filter(it -> !it.isEmpty())
+                    .map(it -> asList(it.split(",")))
+                    .ifPresent(contributors -> setManifestContributors(contributors.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(ManifestContributor.class::cast).collect(toList())));
+            ofNullable(properties.getProperty("winegrower.ripener.configuration.jarFilter"))
+                    .map(String::valueOf)
+                    .filter(it -> !it.isEmpty())
+                    .ifPresent(filter -> {
+                        try {
+                            setJarFilter((Predicate<String>) Thread.currentThread().getContextClassLoader().loadClass(filter)
+                                    .getConstructor().newInstance());
+                        } catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
+                            throw new IllegalArgumentException(e);
+                        } catch (final InvocationTargetException e) {
+                            throw new IllegalArgumentException(e.getTargetException());
+                        }
+                    });
+        }
+    }
 
     class Impl implements Ripener {
         private static final Logger LOGGER = LoggerFactory.getLogger(Ripener.class);
@@ -207,6 +273,7 @@ public interface Ripener extends AutoCloseable {
         private final Configuration configuration;
 
         private long startTime = -1;
+        private StandaloneScanner scanner;
 
         public Impl(final Configuration configuration) {
             this.configuration = configuration;
@@ -222,7 +289,8 @@ public interface Ripener extends AutoCloseable {
             registerBuiltInService(EventAdmin.class, this.eventAdmin, new Hashtable<>());
             registerBuiltInService(org.osgi.service.log.LoggerFactory.class, loadLoggerFactory(), new Hashtable<>());
 
-            try (final InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("winegrower.properties")) {
+            try (final InputStream stream = Thread.currentThread().getContextClassLoader()
+                    .getResourceAsStream("winegrower.properties")) {
                 loadConfiguration(stream);
             } catch (final IOException e) {
                 LOGGER.warn(e.getMessage());
@@ -367,7 +435,10 @@ public interface Ripener extends AutoCloseable {
             startTime = System.currentTimeMillis();
             LOGGER.info("Starting Apache Winegrower application on {}",
                     LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneId.systemDefault()));
-            final StandaloneScanner scanner = new StandaloneScanner(configuration, registry.getFramework());
+            if (configuration.isLazyInstall()) {
+                return this;
+            }
+            final StandaloneScanner scanner = getScanner();
             final AtomicLong bundleIdGenerator = new AtomicLong(1);
             Stream.concat(Stream.concat(
                     scanner.findOSGiBundles().stream(),
@@ -382,9 +453,14 @@ public interface Ripener extends AutoCloseable {
                     .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;
         }
 
+        public synchronized StandaloneScanner getScanner() {
+            return scanner == null ? scanner = new StandaloneScanner(configuration, registry.getFramework()) : scanner;
+        }
+
         @Override
         public synchronized void stop() {
             LOGGER.info("Stopping Apache Winegrower application on {}", LocalDateTime.now());
@@ -474,6 +550,7 @@ public interface Ripener extends AutoCloseable {
                     .map(it -> configuration.getPrioritizedBundles().indexOf(it))
                     .orElse(-1);
         }
+
     }
 
     static Ripener create(final Configuration configuration) {
@@ -484,59 +561,7 @@ public interface Ripener extends AutoCloseable {
     static void main(final String[] args) {
         final CountDownLatch latch = new CountDownLatch(1);
         final Configuration configuration = new Configuration();
-        ofNullable(System.getProperty("winegrower.ripener.configuration.workdir"))
-                .map(String::valueOf)
-                .map(File::new)
-                .ifPresent(configuration::setWorkDir);
-        ofNullable(System.getProperty("winegrower.ripener.configuration.prioritizedBundles"))
-                .map(String::valueOf)
-                .filter(it -> !it.isEmpty())
-                .map(it -> asList(it.split(",")))
-                .ifPresent(configuration::setPrioritizedBundles);
-        ofNullable(System.getProperty("winegrower.ripener.configuration.ignoredBundles"))
-                .map(String::valueOf)
-                .filter(it -> !it.isEmpty())
-                .map(it -> asList(it.split(",")))
-                .ifPresent(configuration::setIgnoredBundles);
-        ofNullable(System.getProperty("winegrower.ripener.configuration.scanningIncludes"))
-                .map(String::valueOf)
-                .filter(it -> !it.isEmpty())
-                .map(it -> asList(it.split(",")))
-                .ifPresent(configuration::setScanningIncludes);
-        ofNullable(System.getProperty("winegrower.ripener.configuration.scanningExcludes"))
-                .map(String::valueOf)
-                .filter(it -> !it.isEmpty())
-                .map(it -> asList(it.split(",")))
-                .ifPresent(configuration::setScanningExcludes);
-        ofNullable(System.getProperty("winegrower.ripener.configuration.manifestContributors"))
-                .map(String::valueOf)
-                .filter(it -> !it.isEmpty())
-                .map(it -> asList(it.split(",")))
-                .ifPresent(contributors -> {
-                    configuration.setManifestContributors(contributors.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(ManifestContributor.class::cast).collect(toList()));
-                });
-        ofNullable(System.getProperty("winegrower.ripener.configuration.jarFilter"))
-                .map(String::valueOf)
-                .filter(it -> !it.isEmpty())
-                .ifPresent(filter -> {
-                    try {
-                        configuration.setJarFilter((Predicate<String>) Thread.currentThread().getContextClassLoader().loadClass(filter)
-                                .getConstructor().newInstance());
-                    } catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
-                        throw new IllegalArgumentException(e);
-                    } catch (final InvocationTargetException e) {
-                        throw new IllegalArgumentException(e.getTargetException());
-                    }
-                });
+        configuration.fromProperties(System.getProperties());
         final Ripener main = new Impl(configuration).start();
         Runtime.getRuntime().addShutdownHook(new Thread() {
 
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 e1f3133..bfeb121 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
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.jar.Manifest;
 import java.util.stream.Stream;
@@ -63,6 +64,8 @@ public class BundleContextImpl implements BundleContext {
     private final Collection<FrameworkListener> frameworkListeners = new CopyOnWriteArrayList<>();
     private final Map<ServiceReference<?>, Object> serviceInstances = new ConcurrentHashMap<>();
 
+    private Function<String, Bundle> installer;
+
     BundleContextImpl(final Manifest manifest, final OSGiServices services, final Supplier<Bundle> bundleSupplier,
                       final BundleRegistry registry) {
         this.manifest = manifest;
@@ -71,6 +74,10 @@ public class BundleContextImpl implements BundleContext {
         this.registry = registry;
     }
 
+    public void setInstaller(final Function<String, Bundle> installer) {
+        this.installer = installer;
+    }
+
     public BundleRegistry getRegistry() {
         return registry;
     }
@@ -103,12 +110,15 @@ public class BundleContextImpl implements BundleContext {
 
     @Override
     public Bundle installBundle(final String location, final InputStream input) throws BundleException {
+        if (installer != null) {
+            return installer.apply(location);
+        }
         throw new BundleException("Unsupported operation");
     }
 
     @Override
     public Bundle installBundle(final String location) throws BundleException {
-        throw new BundleException("Unsupported operation");
+        return installBundle(location, null);
     }
 
     @Override
diff --git a/winegrower-core/src/main/java/org/apache/winegrower/framework/WinegrowerFramework.java b/winegrower-core/src/main/java/org/apache/winegrower/framework/WinegrowerFramework.java
new file mode 100644
index 0000000..32a2279
--- /dev/null
+++ b/winegrower-core/src/main/java/org/apache/winegrower/framework/WinegrowerFramework.java
@@ -0,0 +1,356 @@
+/**
+ * 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.framework;
+
+import org.apache.winegrower.Ripener;
+import org.apache.winegrower.deployer.BundleContextImpl;
+import org.apache.winegrower.deployer.BundleImpl;
+import org.apache.winegrower.deployer.OSGiBundleLifecycle;
+import org.apache.winegrower.scanner.StandaloneScanner;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.Version;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.cert.X509Certificate;
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import static java.util.Optional.ofNullable;
+
+public class WinegrowerFramework implements Framework {
+    private volatile int state = INSTALLED;
+
+    private final AtomicLong bundleIdGenerator = new AtomicLong(1);
+
+    private Ripener ripener;
+    private Ripener.Configuration configuration = new Ripener.Configuration();
+    private FrameworkListener[] listeners;
+    private BundleImpl frameworkBundle;
+
+    public WinegrowerFramework() {
+        configuration.setLazyInstall(true);
+    }
+
+    public void setConfiguration(final Ripener.Configuration configuration) {
+        this.configuration = configuration;
+        this.configuration.fromProperties(System.getProperties());
+    }
+
+    public void setConfigurationProperties(final Properties configuration) {
+        this.configuration.fromProperties(configuration);
+    }
+
+    @Override
+    public void init() {
+        init(new FrameworkListener[0]);
+    }
+
+    @Override
+    public void init(final FrameworkListener... listeners) {
+        ripener = Ripener.create(configuration);
+        frameworkBundle = ripener.getRegistry().getBundles().get(0L).getBundle();
+        BundleContextImpl.class.cast(getBundleContext()).setInstaller(this::installBundle);
+        this.listeners = listeners;
+        state = INSTALLED;
+        fireFrameworkEvent(null);
+    }
+
+    @Override
+    public FrameworkEvent waitForStop(final long timeout) throws InterruptedException {
+        final Clock clock = Clock.systemUTC();
+        final Instant end = clock.instant().plusMillis(timeout);
+        while (clock.instant().isBefore(end)) {
+            switch (state) {
+                case ACTIVE:
+                case RESOLVED:
+                case INSTALLED:
+                    Thread.sleep(250);
+                default:
+                    break;
+            }
+        }
+        return new FrameworkEvent(state, getFrameworkBundle(), null);
+    }
+
+    @Override
+    public void start() throws BundleException {
+        start(ACTIVE);
+    }
+
+    @Override
+    public int getState() {
+        return state;
+    }
+
+    @Override
+    public void start(final int options) throws BundleException {
+        state = STARTING;
+        fireFrameworkEvent(null);
+        try {
+            ripener.start();
+            state = ACTIVE;
+            fireFrameworkEvent(null);
+        } catch (final RuntimeException re) {
+            state = UNINSTALLED;
+            fireFrameworkEvent(re);
+            throw re;
+        } catch (final Exception e) {
+            state = UNINSTALLED;
+            fireFrameworkEvent(e);
+            throw new BundleException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void stop() throws BundleException {
+        stop(STOP_TRANSIENT);
+    }
+
+    @Override
+    public void stop(final int options) throws BundleException {
+        state = STOPPING;
+        fireFrameworkEvent(null);
+        try {
+            ripener.stop();
+            state = STOP_TRANSIENT;
+            fireFrameworkEvent(null);
+        } catch (final RuntimeException re) {
+            state = UNINSTALLED;
+            fireFrameworkEvent(re);
+            throw re;
+        } catch (final Exception e) {
+            state = UNINSTALLED;
+            fireFrameworkEvent(e);
+            throw new BundleException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void uninstall() {
+        state = Framework.UNINSTALLED;
+        final Map<Long, OSGiBundleLifecycle> bundles = ripener.getRegistry().getBundles();
+        bundles.entrySet().stream()
+                .filter(it -> it.getKey() > 0)
+                .forEach(e -> e.getValue().stop());
+        final OSGiBundleLifecycle fwk = bundles.remove(0L);
+        bundles.clear();
+        bundles.put(0L, fwk);
+        fireFrameworkEvent(null);
+    }
+
+    @Override
+    public Dictionary<String, String> getHeaders() {
+        return getHeaders(null);
+    }
+
+    @Override
+    public void update() {
+        // no-op
+    }
+
+    @Override
+    public void update(final InputStream in) {
+        // no-op
+    }
+
+    @Override
+    public long getBundleId() {
+        return 0;
+    }
+
+    @Override
+    public String getLocation() {
+        return "system";
+    }
+
+    @Override
+    public ServiceReference<?>[] getRegisteredServices() {
+        return ripener.getServices().getServices().stream()
+                .map(ServiceRegistration::getReference)
+                .toArray(ServiceReference[]::new);
+    }
+
+    @Override
+    public ServiceReference<?>[] getServicesInUse() {
+        return getRegisteredServices();
+    }
+
+    @Override
+    public boolean hasPermission(final Object permission) {
+        return true;
+    }
+
+    @Override
+    public URL getResource(final String name) {
+        return getFrameworkBundle().getResource(name);
+    }
+
+    @Override
+    public Dictionary<String, String> getHeaders(final String locale) {
+        return getFrameworkBundle().getHeaders(locale);
+    }
+
+    @Override
+    public String getSymbolicName() {
+        return getFrameworkBundle().getSymbolicName();
+    }
+
+    @Override
+    public Class<?> loadClass(final String name) throws ClassNotFoundException {
+        return getFrameworkBundle().loadClass(name);
+    }
+
+    @Override
+    public Enumeration<URL> getResources(final String name) throws IOException {
+        return getFrameworkBundle().getResources(name);
+    }
+
+    @Override
+    public Enumeration<String> getEntryPaths(final String path) {
+        return getFrameworkBundle().getEntryPaths(path);
+    }
+
+    @Override
+    public URL getEntry(final String path) {
+        return getFrameworkBundle().getEntry(path);
+    }
+
+    @Override
+    public long getLastModified() {
+        return getFrameworkBundle().getLastModified();
+    }
+
+    @Override
+    public Enumeration<URL> findEntries(final String path, final String filePattern, final boolean recurse) {
+        return getFrameworkBundle().findEntries(path, filePattern, recurse);
+    }
+
+    @Override
+    public BundleContext getBundleContext() {
+        return getFrameworkBundle().getBundleContext();
+    }
+
+    @Override
+    public Map<X509Certificate, List<X509Certificate>> getSignerCertificates(final int signersType) {
+        return getFrameworkBundle().getSignerCertificates(signersType);
+    }
+
+    @Override
+    public Version getVersion() {
+        return getFrameworkBundle().getVersion();
+    }
+
+    @Override
+    public <A> A adapt(final Class<A> type) {
+        return getFrameworkBundle().adapt(type);
+    }
+
+    @Override
+    public File getDataFile(final String filename) {
+        return getFrameworkBundle().getDataFile(filename);
+    }
+
+    @Override
+    public int compareTo(final Bundle o) {
+        return getFrameworkBundle().compareTo(o);
+    }
+
+    private Bundle installBundle(final String location) {
+        final StandaloneScanner scanner = Ripener.Impl.class.cast(ripener).getScanner();
+        final StandaloneScanner.BundleDefinition bundleDefinition = Stream.concat(
+                scanner.findOSGiBundles().stream(),
+                scanner.findPotentialOSGiBundles().stream())
+                .filter(bundle -> doesLocationMatches(bundle, location))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("No bundle found for " + location + ", available:\n\n" +
+                        scanner.findOSGiBundles() + "\n" +
+                        scanner.findPotentialOSGiBundles()));
+        final OSGiBundleLifecycle lifecycle = new OSGiBundleLifecycle(
+                bundleDefinition.getManifest(), bundleDefinition.getJar(),
+                ripener.getServices(), ripener.getRegistry(), configuration,
+                bundleIdGenerator.getAndIncrement(),
+                bundleDefinition.getFiles());
+        lifecycle.start();
+        ripener.getRegistry().getBundles().put(lifecycle.getBundle().getBundleId(), lifecycle);
+        return lifecycle.getBundle();
+    }
+
+    // todo: enhance with mvn:, file:// support
+    private boolean doesLocationMatches(final StandaloneScanner.BundleDefinition bundle, final String location) {
+        if (bundle.getJar() != null) {
+            final boolean direct = location.contains(bundle.getJar().getAbsolutePath());
+            if (direct) {
+                return true;
+            }
+
+            final String normalizedName = location.replace(File.separatorChar, '/');
+            if (!normalizedName.contains("/")) {
+                return bundle.getJar().getName().equals(normalizedName);
+            }
+
+            try {
+                return bundle.getJar().toURI().toURL().toExternalForm().equals(location);
+            } catch (final MalformedURLException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private BundleImpl getFrameworkBundle() {
+        return frameworkBundle;
+    }
+
+    private void fireFrameworkEvent(final Throwable error) { // todo: shouldn't really be state
+        final FrameworkEvent event = new FrameworkEvent(error != null ? FrameworkEvent.ERROR : state, getFrameworkBundle(), error);
+        ofNullable(listeners).map(Stream::of).orElseGet(Stream::empty).forEach(l -> l.frameworkEvent(event));
+    }
+
+    public static class Factory implements FrameworkFactory {
+        @Override
+        public Framework newFramework(final Map<String, String> configuration) {
+            final WinegrowerFramework framework = new WinegrowerFramework();
+            ofNullable(configuration)
+                    .map(c -> c.entrySet().stream().collect(Collector.of(
+                            Properties::new,
+                            (p, i) -> p.setProperty(i.getKey(), i.getValue()),
+                            (p1, p2) -> {
+                                p1.putAll(p2);
+                                return p1;
+                            })))
+                    .ifPresent(framework.configuration::fromProperties);
+            return framework;
+        }
+    }
+}
diff --git a/winegrower-core/src/main/java/org/apache/winegrower/scanner/StandaloneScanner.java b/winegrower-core/src/main/java/org/apache/winegrower/scanner/StandaloneScanner.java
index 9b3cfb2..eee1829 100644
--- a/winegrower-core/src/main/java/org/apache/winegrower/scanner/StandaloneScanner.java
+++ b/winegrower-core/src/main/java/org/apache/winegrower/scanner/StandaloneScanner.java
@@ -62,6 +62,9 @@ public class StandaloneScanner {
     private final Map<String, Manifest> providedManifests;
     private final Map<String, List<String>> providedIndex;
 
+    private List<BundleDefinition> potentialBundles;
+    private List<BundleDefinition> bundles;
+
     public StandaloneScanner(final Ripener.Configuration configuration, final File frameworkJar) {
         this.configuration = configuration;
         this.frameworkJar = frameworkJar;
@@ -152,8 +155,11 @@ public class StandaloneScanner {
     }
 
     public Collection<BundleDefinition> findPotentialOSGiBundles() {
+        if (potentialBundles != null) {
+            return potentialBundles;
+        }
         final KnownJarsFilter filter = new KnownJarsFilter(configuration);
-        return urls.stream()
+        return potentialBundles = urls.stream()
               .map(it -> new FileAndUrl(Files.toFile(it), it))
               .filter(it -> !it.file.getAbsoluteFile().equals(frameworkJar))
               .filter(it -> filter.test(it.file.getName()))
@@ -179,7 +185,10 @@ public class StandaloneScanner {
     }
 
     public Collection<BundleDefinition> findOSGiBundles() {
-        return Stream.concat(
+        if (bundles != null) {
+            return bundles;
+        }
+        return bundles = Stream.concat(
                     urls.stream()
                         .map(Files::toFile)
                         .filter(this::isIncluded)
@@ -265,6 +274,13 @@ public class StandaloneScanner {
         public File getJar() {
             return jar;
         }
+
+        @Override
+        public String toString() {
+            return "BundleDefinition{" +
+                    "jar=" + jar +
+                    '}';
+        }
     }
 
     private static class FileAndUrl {
diff --git a/winegrower-core/src/test/java/org/apache/winegrower/framework/WinegrowerFrameworkTest.java b/winegrower-core/src/test/java/org/apache/winegrower/framework/WinegrowerFrameworkTest.java
new file mode 100644
index 0000000..b302d2d
--- /dev/null
+++ b/winegrower-core/src/test/java/org/apache/winegrower/framework/WinegrowerFrameworkTest.java
@@ -0,0 +1,46 @@
+/**
+ * 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.framework;
+
+import org.junit.jupiter.api.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.launch.Framework;
+
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class WinegrowerFrameworkTest {
+    @Test
+    public void run() throws BundleException {
+        final Framework framework = new WinegrowerFramework();
+        { // let this specific API not be exposed in the other parts of the test
+            WinegrowerFramework.class.cast(framework).setConfigurationProperties(new Properties() {{
+                setProperty("winegrower.ripener.configuration.scanningExcludes", "test-classes");
+            }});
+        }
+        assertEquals(Framework.INSTALLED, framework.getState());
+        framework.init();
+        assertEquals(Framework.INSTALLED, framework.getState());
+        framework.start();
+        assertEquals(Framework.ACTIVE, framework.getState());
+        final Bundle[] bundles = framework.getBundleContext().getBundles();
+        assertEquals(1, bundles.length);
+        framework.getBundleContext().installBundle("org.apache.aries.cdi.extra-1.1.0.jar");
+        assertEquals(2, framework.getBundleContext().getBundles().length);
+        framework.stop();
+        assertEquals(Framework.STOP_TRANSIENT, framework.getState());
+    }
+}