blob: 2503ab4e38ba5f96169befff556def4071e45719 [file] [log] [blame]
/*
* Copyright 2015 The Apache Software Foundation.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.brooklyn.rt.felix;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import com.google.common.base.Stopwatch;
import com.google.common.reflect.ClassPath;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.ReferenceWithError;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.osgi.OsgiUtils;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.apache.felix.framework.FrameworkFactory;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.launch.Framework;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Functions for starting an Apache Felix OSGi framework inside a non-OSGi Brooklyn distro.
*
* @author Ciprian Ciubotariu <cheepeero@gmx.net>
*/
public class EmbeddedFelixFramework {
private static final Logger LOG = LoggerFactory.getLogger(EmbeddedFelixFramework.class);
private static final String EXTENSION_PROTOCOL = "system";
private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF";
private static final Set<String> SYSTEM_BUNDLES = MutableSet.of();
// set here to avoid importing core, only needed for tests
private static final String BROOKLYN_VERSION = "1.2.0-SNAPSHOT";
private static final String BROOKLYN_VERSION_OSGI_ROUGH = BROOKLYN_VERSION.replaceFirst("-.*", "");
private static final Set<URL> CANDIDATE_BOOT_BUNDLES;
static {
try {
CANDIDATE_BOOT_BUNDLES = MutableSet.copyOf(Collections.list(
EmbeddedFelixFramework.class.getClassLoader().getResources(MANIFEST_PATH))).asUnmodifiable();
} catch (Exception e) {
// should never happen; weird classloading problem
throw Exceptions.propagate(e);
}
}
// -------- creating
/*
* loading framework factory and starting framework based on:
* http://felix.apache.org/documentation/subprojects/apache-felix-framework/apache-felix-framework-launching-and-embedding.html
*/
public static FrameworkFactory newFrameworkFactory() {
URL url = EmbeddedFelixFramework.class.getClassLoader().getResource(
"META-INF/services/org.osgi.framework.launch.FrameworkFactory");
if (url != null) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
try {
for (String s = br.readLine(); s != null; s = br.readLine()) {
s = s.trim();
// load the first non-empty, non-commented line
if ((s.length() > 0) && (s.charAt(0) != '#')) {
return (FrameworkFactory) Class.forName(s).newInstance();
}
}
} finally {
if (br != null) br.close();
}
} catch (Exception e) {
// class creation exceptions are not interesting to caller...
throw Exceptions.propagate(e);
}
}
throw new IllegalStateException("Could not find framework factory.");
}
public static Framework newFrameworkStarted(String felixCacheDir, boolean clean, Map<?,?> extraStartupConfig) {
Map<Object,Object> cfg = MutableMap.copyOf(extraStartupConfig);
if (clean) cfg.put(Constants.FRAMEWORK_STORAGE_CLEAN, "onFirstInit");
if (felixCacheDir!=null) cfg.put(Constants.FRAMEWORK_STORAGE, felixCacheDir);
cfg.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MULTIPLE);
if (CANDIDATE_BOOT_BUNDLES.stream().noneMatch(url -> url.toString().contains("brooklyn-core"))) {
// if not running brooklyn-core from a jar, for osgi deps to work we need to make the system bundle export brooklyn packages;
// mainly for tests to work; everything else should be running from jars with manifest.mf
try {
// spring has: PathMatchingResourcePatternResolver; we use guava's ClassPath
Set<String> brooklynPackages = ClassPath.from(EmbeddedFelixFramework.class.getClassLoader()).getTopLevelClasses()
.stream().map(c -> c.getPackageName())
.filter(n -> n.startsWith("org.apache.brooklyn.")).collect(Collectors.toSet());
LOG.info("Embedded felix OSGi system running without brooklyn-core JAR; manually adding brooklyn packages ("+brooklynPackages.size()+") to system bundle exports");
cfg.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, brooklynPackages.stream().map(p -> p + ";version=\""+BROOKLYN_VERSION_OSGI_ROUGH+"\"").collect(Collectors.joining(",")));
} catch (Exception e) {
throw Exceptions.propagateAnnotated("Unable to set up embedded felix framework with packages inferred", e);
}
}
FrameworkFactory factory = newFrameworkFactory();
Stopwatch timer = Stopwatch.createStarted();
Framework framework = factory.newFramework(cfg);
try {
framework.init();
installBootBundles(framework);
framework.start();
} catch (Exception e) {
// framework bundle start exceptions are not interesting to caller...
throw Exceptions.propagate(e);
}
LOG.debug("System bundles are: "+SYSTEM_BUNDLES);
LOG.debug("OSGi framework started in " + Duration.of(timer));
return framework;
}
public static void stopFramework(Framework framework) throws RuntimeException {
try {
if (framework != null) {
framework.stop();
framework.waitForStop(0);
}
} catch (BundleException | InterruptedException e) {
throw Exceptions.propagate(e);
}
}
/* --- helper functions */
private static void installBootBundles(Framework framework) {
Stopwatch timer = Stopwatch.createStarted();
LOG.debug("Installing OSGi boot bundles from "+EmbeddedFelixFramework.class.getClassLoader()+"...");
Iterator<URL> resources = CANDIDATE_BOOT_BUNDLES.iterator();
// previously we evaluated this each time, but lately (discovered in 2019,
// possibly the case for a long time before) it seems to grow, accessing ad hoc dirs
// in cache/* made by tests, which get deleted, logging lots of errors.
// so now we statically populate it at load time.
BundleContext bundleContext = framework.getBundleContext();
Map<String, Bundle> installedBundles = getInstalledBundlesById(bundleContext);
while (resources.hasNext()) {
URL url = resources.next();
ReferenceWithError<?> installResult = installExtensionBundle(bundleContext, url, installedBundles, OsgiUtils.getVersionedId(framework));
if (installResult.hasError() && !installResult.masksErrorIfPresent()) {
// these are just candiate boot bundles used in testing so forgive errors and warnings
if (LOG.isTraceEnabled()) LOG.trace("Unable to install manifest from "+url+": "+installResult.getError(), installResult.getError());
} else {
Object result = installResult.getWithoutError();
if (result instanceof Bundle) {
String v = OsgiUtils.getVersionedId( (Bundle)result );
SYSTEM_BUNDLES.add(v);
if (installResult.hasError()) {
if (LOG.isTraceEnabled()) LOG.trace(installResult.getError().getMessage()+(result!=null ? " ("+result+"/"+v+")" : ""));
} else {
if (LOG.isTraceEnabled()) LOG.trace("Installed "+v+" from "+url);
}
} else if (installResult.hasError()) {
LOG.trace(installResult.getError().getMessage());
}
}
}
if (LOG.isTraceEnabled()) LOG.trace("Installed OSGi boot bundles in "+Time.makeTimeStringRounded(timer)+": "+Arrays.asList(framework.getBundleContext().getBundles()));
}
private static Map<String, Bundle> getInstalledBundlesById(BundleContext bundleContext) {
Map<String, Bundle> installedBundles = new HashMap<String, Bundle>();
Bundle[] bundles = bundleContext.getBundles();
for (Bundle b : bundles) {
installedBundles.put(OsgiUtils.getVersionedId(b), b);
}
return installedBundles;
}
/** Wraps the bundle if successful or already installed, wraps TRUE if it's the system entry,
* wraps null if the bundle is already installed from somewhere else;
* in all these cases <i>masking</i> an explanatory error if already installed or it's the system entry.
* <p>
* Returns an instance wrapping null and <i>throwing</i> an error if the bundle could not be installed.
*/
private static ReferenceWithError<?> installExtensionBundle(BundleContext bundleContext, URL manifestUrl, Map<String, Bundle> installedBundles, String frameworkVersionedId) {
//ignore http://felix.extensions:9/ system entry
if("felix.extensions".equals(manifestUrl.getHost()))
return ReferenceWithError.newInstanceMaskingError(null, new IllegalArgumentException("Skipping install of internal extension bundle from "+manifestUrl));
try {
Manifest manifest = readManifest(manifestUrl);
if (!isValidBundle(manifest))
return ReferenceWithError.newInstanceMaskingError(null, new IllegalArgumentException("Resource at "+manifestUrl+" is not an OSGi bundle manifest"));
String versionedId = OsgiUtils.getVersionedId(manifest);
URL bundleUrl = OsgiUtils.getContainerUrl(manifestUrl, MANIFEST_PATH);
Bundle existingBundle = installedBundles.get(versionedId);
if (existingBundle != null) {
if (!bundleUrl.equals(existingBundle.getLocation()) &&
//the framework bundle is always pre-installed, don't display duplicate info
!versionedId.equals(frameworkVersionedId)) {
return ReferenceWithError.newInstanceMaskingError(null, new IllegalArgumentException("Bundle "+versionedId+" (from manifest " + manifestUrl + ") is already installed, from " + existingBundle.getLocation()));
}
return ReferenceWithError.newInstanceMaskingError(existingBundle, new IllegalArgumentException("Bundle "+versionedId+" from manifest " + manifestUrl + " is already installed"));
}
byte[] jar = buildExtensionBundle(manifest);
if (LOG.isTraceEnabled()) LOG.trace("Installing boot bundle " + bundleUrl);
//mark the bundle as extension so we can detect it later using the "system:" protocol
//(since we cannot access BundleImpl.isExtension)
Bundle newBundle = bundleContext.installBundle(EXTENSION_PROTOCOL + ":" + bundleUrl.toString(), new ByteArrayInputStream(jar));
installedBundles.put(versionedId, newBundle);
return ReferenceWithError.newInstanceWithoutError(newBundle);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
return ReferenceWithError.newInstanceThrowingError(null,
new IllegalStateException("Problem installing extension bundle " + manifestUrl + ": "+e, e));
}
}
private static Manifest readManifest(URL manifestUrl) throws IOException {
Manifest manifest;
InputStream in = null;
try {
in = manifestUrl.openStream();
manifest = new Manifest(in);
} finally {
if (in != null) {
try {in.close();}
catch (Exception e) {};
}
}
return manifest;
}
private static byte[] buildExtensionBundle(Manifest manifest) throws IOException {
Attributes atts = manifest.getMainAttributes();
//the following properties are invalid in extension bundles
atts.remove(new Attributes.Name(Constants.IMPORT_PACKAGE));
atts.remove(new Attributes.Name(Constants.REQUIRE_BUNDLE));
atts.remove(new Attributes.Name(Constants.BUNDLE_NATIVECODE));
atts.remove(new Attributes.Name(Constants.DYNAMICIMPORT_PACKAGE));
atts.remove(new Attributes.Name(Constants.BUNDLE_ACTIVATOR));
//mark as extension bundle
atts.putValue(Constants.FRAGMENT_HOST, "system.bundle; extension:=framework");
//create the jar containing the manifest
ByteArrayOutputStream jar = new ByteArrayOutputStream();
JarOutputStream out = new JarOutputStream(jar, manifest);
out.close();
return jar.toByteArray();
}
private static boolean isValidBundle(Manifest manifest) {
Attributes atts = manifest.getMainAttributes();
return atts.containsKey(new Attributes.Name(Constants.BUNDLE_MANIFESTVERSION));
}
public static boolean isExtensionBundle(Bundle bundle) {
String location = bundle.getLocation();
return location != null &&
EXTENSION_PROTOCOL.equals(Urls.getProtocol(location));
}
}