/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.util.core.osgi;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
import org.apache.brooklyn.rt.felix.EmbeddedFelixFramework;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.osgi.OsgiUtils;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Strings;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.Version;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
/**
* utilities for working with osgi.
* osgi support is in early days (June 2014) so this class is beta, subject to change,
* particularly in how framework is started and bundles installed.
*
* @since 0.7.0 */
@Beta
public class Osgis {
private static final Logger LOG = LoggerFactory.getLogger(Osgis.class);
/** @deprecated since 0.9.0, replaced with {@link org.apache.brooklyn.util.osgi.VersionedName} */
@Deprecated
public static class VersionedName extends org.apache.brooklyn.util.osgi.VersionedName {
private VersionedName(org.apache.brooklyn.util.osgi.VersionedName src) {
super(src.getSymbolicName(), src.getVersion());
}
public VersionedName(Bundle b) {
super(b);
}
public VersionedName(String symbolicName, Version version) {
super(symbolicName, version);
}
}
public static class BundleFinder {
protected final Framework framework;
protected String symbolicName;
protected String version;
protected String url;
protected boolean urlMandatory = false;
protected final List<Predicate<? super Bundle>> predicates = MutableList.of();
protected BundleFinder(Framework framework) {
this.framework = framework;
}
public BundleFinder symbolicName(String symbolicName) {
this.symbolicName = symbolicName;
return this;
}
public BundleFinder version(String version) {
this.version = version;
return this;
}
public BundleFinder id(String symbolicNameOptionallyWithVersion) {
if (Strings.isBlank(symbolicNameOptionallyWithVersion))
return this;
Maybe<org.apache.brooklyn.util.osgi.VersionedName> nv = OsgiUtils.parseOsgiIdentifier(symbolicNameOptionallyWithVersion);
if (nv.isAbsent())
throw new IllegalArgumentException("Cannot parse symbolic-name:version string '"+symbolicNameOptionallyWithVersion+"'");
return id(nv.get());
}
private BundleFinder id(org.apache.brooklyn.util.osgi.VersionedName nv) {
symbolicName(nv.getSymbolicName());
if (nv.getVersion() != null) {
version(nv.getVersion().toString());
}
return this;
}
public BundleFinder bundle(CatalogBundle bundle) {
if (bundle.isNameResolved()) {
symbolicName(bundle.getSymbolicName());
version(bundle.getVersion());
}
if (bundle.getUrl() != null) {
requiringFromUrl(bundle.getUrl());
}
return this;
}
/** Looks for a bundle matching the given URL;
* unlike {@link #requiringFromUrl(String)} however, if the URL does not match any bundles
* it will return other matching bundles <i>if</if> a {@link #symbolicName(String)} is specified.
*/
public BundleFinder preferringFromUrl(String url) {
this.url = url;
urlMandatory = false;
return this;
}
/** Requires the bundle to have the given URL set as its location. */
public BundleFinder requiringFromUrl(String url) {
this.url = url;
urlMandatory = true;
return this;
}
/** Finds the best matching bundle. */
public Maybe<Bundle> find() {
return findOne(false);
}
/** Finds the matching bundle, requiring it to be unique. */
public Maybe<Bundle> findUnique() {
return findOne(true);
}
protected Maybe<Bundle> findOne(boolean requireExactlyOne) {
if (symbolicName==null && url==null)
throw new IllegalStateException(this+" must be given either a symbolic name or a URL");
List<Bundle> result = findAll();
if (result.isEmpty())
return Maybe.absent("No bundle matching "+getConstraintsDescription());
if (requireExactlyOne && result.size()>1)
return Maybe.absent("Multiple bundles ("+result.size()+") matching "+getConstraintsDescription());
return Maybe.of(result.get(0));
}
/** Finds all matching bundles, in decreasing version order. */
public List<Bundle> findAll() {
boolean urlMatched = false;
List<Bundle> result = MutableList.of();
for (Bundle b: framework.getBundleContext().getBundles()) {
if (symbolicName!=null && !symbolicName.equals(b.getSymbolicName())) continue;
if (version!=null && !Version.parseVersion(version).equals(b.getVersion())) continue;
for (Predicate<? super Bundle> predicate: predicates) {
if (!predicate.apply(b)) continue;
}
// check url last, because if it isn't mandatory we should only clear if we find a url
// for which the other items also match
if (url!=null) {
boolean matches = url.equals(b.getLocation());
if (urlMandatory) {
if (!matches) continue;
else urlMatched = true;
} else {
if (matches) {
if (!urlMatched) {
result.clear();
urlMatched = true;
}
} else {
if (urlMatched) {
// can't use this bundle as we have previously found a preferred bundle, with a matching url
continue;
}
}
}
}
result.add(b);
}
if (symbolicName==null && url!=null && !urlMatched) {
// if we only "preferred" the url, and we did not match it, and we did not have a symbolic name,
// then clear the results list!
result.clear();
}
Collections.sort(result, new Comparator<Bundle>() {
@Override
public int compare(Bundle o1, Bundle o2) {
return o2.getVersion().compareTo(o1.getVersion());
}
});
return result;
}
public String getConstraintsDescription() {
List<String> parts = MutableList.of();
if (symbolicName!=null) parts.add("symbolicName="+symbolicName);
if (version!=null) parts.add("version="+version);
if (url!=null)
parts.add("url["+(urlMandatory ? "required" : "preferred")+"]="+url);
if (!predicates.isEmpty())
parts.add("predicates="+predicates);
return Joiner.on(";").join(parts);
}
@Override
public String toString() {
return getClass().getCanonicalName()+"["+getConstraintsDescription()+"]";
}
public BundleFinder version(final Predicate<Version> versionPredicate) {
return satisfying(new Predicate<Bundle>() {
@Override
public boolean apply(Bundle input) {
return versionPredicate.apply(input.getVersion());
}
});
}
public BundleFinder satisfying(Predicate<? super Bundle> predicate) {
predicates.add(predicate);
return this;
}
}
public static BundleFinder bundleFinder(Framework framework) {
return new BundleFinder(framework);
}
/** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
public static List<Bundle> getBundlesByName(Framework framework, String symbolicName, Predicate<Version> versionMatcher) {
return bundleFinder(framework).symbolicName(symbolicName).version(versionMatcher).findAll();
}
/** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
public static List<Bundle> getBundlesByName(Framework framework, String symbolicName) {
return bundleFinder(framework).symbolicName(symbolicName).findAll();
}
/**
* Tries to find a bundle in the given framework with name matching either `name' or `name:version'.
* @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
public static Maybe<Bundle> getBundle(Framework framework, String symbolicNameOptionallyWithVersion) {
return bundleFinder(framework).id(symbolicNameOptionallyWithVersion).find();
}
/** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
public static Maybe<Bundle> getBundle(Framework framework, String symbolicName, String version) {
return bundleFinder(framework).symbolicName(symbolicName).version(version).find();
}
/** @deprecated since 0.7.0 use {@link #bundleFinder(Framework)} */ @Deprecated
public static Maybe<Bundle> getBundle(Framework framework, String symbolicName, Version version) {
return bundleFinder(framework).symbolicName(symbolicName).version(Predicates.equalTo(version)).findUnique();
}
/** @deprecated since 0.9.0, replaced by {@link EmbeddedFelixFramework#newFrameworkFactory() */
@Deprecated
public static FrameworkFactory newFrameworkFactory() {
return EmbeddedFelixFramework.newFrameworkFactory();
}
/** @deprecated since 0.9.0, replaced by {@link #getFramework(java.lang.String, boolean) } */
@Deprecated
public static Framework newFrameworkStarted(String felixCacheDir, boolean clean, Map<?,?> extraStartupConfig) {
return getFramework(felixCacheDir, clean);
}
/**
* Provides an OSGI framework.
*
* When running inside an OSGi container, the container framework is returned.
* When running standalone a new Apache Felix container is created.
*
* @param felixCacheDir
* @param clean
* @return
* @todo Use felixCacheDir ?
*/
public static Framework getFramework(String felixCacheDir, boolean clean) {
final Bundle bundle = FrameworkUtil.getBundle(Osgis.class);
if (bundle != null) {
// already running inside an OSGi container
return (Framework) bundle.getBundleContext().getBundle(0);
} else {
// not running inside OSGi container
return EmbeddedFelixFramework.newFrameworkStarted(felixCacheDir, clean, null);
}
}
/**
* Stops/ungets the OSGi framework.
*
* See {@link #getFramework(java.lang.String, boolean)}
*
* @param framework
*/
public static void ungetFramework(Framework framework) {
final Bundle bundle = FrameworkUtil.getBundle(Osgis.class);
if (bundle == null) {
EmbeddedFelixFramework.stopFramework(framework);
}
}
/** Tells if Brooklyn is running in an OSGi environment or not. */
public static boolean isBrooklynInsideFramework() {
return FrameworkUtil.getBundle(Osgis.class) != null;
}
/** @deprecated since 0.9.0, replaced with {@link OsgiUtils#getVersionedId(org.osgi.framework.Bundle) } */
@Deprecated
public static String getVersionedId(Bundle b) {
return OsgiUtils.getVersionedId(b);
}
/** @deprecated since 0.9.0, replaced with {@link OsgiUtils#getVersionedId(java.util.jar.Manifest) } */
@Deprecated
public static String getVersionedId(Manifest manifest) {
return OsgiUtils.getVersionedId(manifest);
}
/**
* Installs a bundle from the given URL, doing a check if already installed, and
* using the {@link ResourceUtils} loader for this project (brooklyn core)
*/
public static Bundle install(Framework framework, String url) throws BundleException {
boolean isLocal = isLocalUrl(url);
String localUrl = url;
if (!isLocal) {
localUrl = cacheFile(url);
}
try {
Bundle bundle = getInstalledBundle(framework, localUrl);
if (bundle != null) {
return bundle;
}
// use our URL resolution so we get classpath items
LOG.debug("Installing bundle into {} from url: {}", framework, url);
InputStream stream = getUrlStream(localUrl);
Bundle installedBundle = framework.getBundleContext().installBundle(url, stream);
return installedBundle;
} finally {
if (!isLocal) {
try {
new File(new URI(localUrl)).delete();
} catch (URISyntaxException e) {
throw Exceptions.propagate(e);
}
}
}
}
private static String cacheFile(String url) {
InputStream in = getUrlStream(url);
File cache = Os.writeToTempFile(in, "bundle-cache", "jar");
return cache.toURI().toString();
}
private static boolean isLocalUrl(String url) {
String protocol = Urls.getProtocol(url);
return "file".equals(protocol) ||
"classpath".equals(protocol) ||
"jar".equals(protocol);
}
private static Bundle getInstalledBundle(Framework framework, String url) {
Bundle bundle = framework.getBundleContext().getBundle(url);
if (bundle != null) {
return bundle;
}
// We now support same version installed multiple times (avail since OSGi 4.3+).
// However we do not support overriding *system* bundles, ie anything already on the classpath.
// If we wanted to disable multiple versions, see comments below, and reference to FRAMEWORK_BSNVERSION_MULTIPLE above.
// Felix already assumes the stream is pointing to a JAR
JarInputStream stream;
try {
stream = new JarInputStream(getUrlStream(url));
} catch (IOException e) {
throw Exceptions.propagate(e);
}
Manifest manifest = stream.getManifest();
Streams.closeQuietly(stream);
if (manifest == null) {
throw new IllegalStateException("Missing manifest file in bundle or not a jar file.");
}
String versionedId = OsgiUtils.getVersionedId(manifest);
for (Bundle installedBundle : framework.getBundleContext().getBundles()) {
if (versionedId.equals(OsgiUtils.getVersionedId(installedBundle))) {
if (EmbeddedFelixFramework.isSystemBundle(installedBundle)) {
LOG.debug("Already have system bundle "+versionedId+" from "+installedBundle+"/"+installedBundle.getLocation()+" when requested "+url+"; not installing");
// "System bundles" (ie things on the classpath) cannot be overridden
return installedBundle;
} else {
LOG.debug("Already have bundle "+versionedId+" from "+installedBundle+"/"+installedBundle.getLocation()+" when requested "+url+"; but it is not a system bundle so proceeding");
// Other bundles can be installed multiple times. To ignore multiples and continue to use the old one,
// just return the installedBundle as done just above for system bundles.
}
}
}
return null;
}
private static InputStream getUrlStream(String url) {
return ResourceUtils.create(Osgis.class).getResourceFromUrl(url);
}
/** @deprecated since 0.9.0, replaced with {@link EmbeddedFelixFramework#isExtensionBundle(Bundle)} */
@Deprecated
public static boolean isExtensionBundle(Bundle bundle) {
return EmbeddedFelixFramework.isExtensionBundle(bundle);
}
/** @deprecated since 0.9.0, replaced with {@link OsgiUtils#parseOsgiIdentifier(java.lang.String) } */
@Deprecated
public static Maybe<VersionedName> parseOsgiIdentifier(String symbolicNameOptionalWithVersion) {
final Maybe<org.apache.brooklyn.util.osgi.VersionedName> original = OsgiUtils.parseOsgiIdentifier(symbolicNameOptionalWithVersion);
return original.transform(new Function<org.apache.brooklyn.util.osgi.VersionedName, VersionedName>() {
@Override
public VersionedName apply(org.apache.brooklyn.util.osgi.VersionedName input) {
return new VersionedName(input);
}
});
}
/** @deprecated since 0.9.0, replaced with {@link org.apache.brooklyn.rt.felix.ManifestHelper} */
@Deprecated
public static class ManifestHelper extends org.apache.brooklyn.rt.felix.ManifestHelper {
}
}