brooklyn.util.osgi.Osgis.java Source code

Java tutorial

Introduction

Here is the source code for brooklyn.util.osgi.Osgis.java

Source

/*
 * 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 brooklyn.util.osgi;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import javax.annotation.Nullable;

import org.apache.felix.framework.FrameworkFactory;
import org.apache.felix.framework.util.StringMap;
import org.apache.felix.framework.util.manifestparser.ManifestParser;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleCapability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import brooklyn.catalog.CatalogItem.CatalogBundle;
import brooklyn.util.ResourceUtils;
import brooklyn.util.collections.MutableList;
import brooklyn.util.collections.MutableMap;
import brooklyn.util.collections.MutableSet;
import brooklyn.util.exceptions.Exceptions;
import brooklyn.util.exceptions.ReferenceWithError;
import brooklyn.util.guava.Maybe;
import brooklyn.util.net.Urls;
import brooklyn.util.os.Os;
import brooklyn.util.stream.Streams;
import brooklyn.util.text.Strings;
import brooklyn.util.time.Duration;
import brooklyn.util.time.Time;

import com.google.common.annotations.Beta;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Stopwatch;

/** 
 * 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);

    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();

    public static class VersionedName {
        private final String symbolicName;
        private final Version version;

        public VersionedName(Bundle b) {
            this.symbolicName = b.getSymbolicName();
            this.version = b.getVersion();
        }

        public VersionedName(String symbolicName, Version version) {
            this.symbolicName = symbolicName;
            this.version = version;
        }

        @Override
        public String toString() {
            return symbolicName + ":" + Strings.toString(version);
        }

        public boolean equals(String sn, String v) {
            return symbolicName.equals(sn)
                    && (version == null && v == null || version != null && version.toString().equals(v));
        }

        public boolean equals(String sn, Version v) {
            return symbolicName.equals(sn)
                    && (version == null && v == null || version != null && version.equals(v));
        }

        public String getSymbolicName() {
            return symbolicName;
        }

        public Version getVersion() {
            return version;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(symbolicName, version);
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof VersionedName))
                return false;
            VersionedName o = (VersionedName) other;
            return Objects.equal(symbolicName, o.symbolicName) && Objects.equal(version, o.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<VersionedName> nv = parseOsgiIdentifier(symbolicNameOptionallyWithVersion);
            if (nv.isAbsent())
                throw new IllegalArgumentException(
                        "Cannot parse symbolic-name:version string '" + symbolicNameOptionallyWithVersion + "'");

            return id(nv.get());
        }

        private BundleFinder id(VersionedName nv) {
            symbolicName(nv.getSymbolicName());
            if (nv.getVersion() != null) {
                version(nv.getVersion().toString());
            }
            return this;
        }

        public BundleFinder bundle(CatalogBundle bundle) {
            if (bundle.isNamed()) {
                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);
        }

        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();
    }

    // -------- 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 = Osgis.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);
        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;
    }

    private static void installBootBundles(Framework framework) {
        Stopwatch timer = Stopwatch.createStarted();
        LOG.debug("Installing OSGi boot bundles from " + Osgis.class.getClassLoader() + "...");
        Enumeration<URL> resources;
        try {
            resources = Osgis.class.getClassLoader().getResources(MANIFEST_PATH);
        } catch (IOException e) {
            throw Exceptions.propagate(e);
        }
        BundleContext bundleContext = framework.getBundleContext();
        Map<String, Bundle> installedBundles = getInstalledBundlesById(bundleContext);
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            ReferenceWithError<?> installResult = installExtensionBundle(bundleContext, url, installedBundles,
                    getVersionedId(framework));
            if (installResult.hasError() && !installResult.masksErrorIfPresent()) {
                // it's reported as a critical error, so warn here
                LOG.warn("Unable to install manifest from " + url + ": " + installResult.getError(),
                        installResult.getError());
            } else {
                Object result = installResult.getWithoutError();
                if (result instanceof Bundle) {
                    String v = getVersionedId((Bundle) result);
                    SYSTEM_BUNDLES.add(v);
                    if (installResult.hasError()) {
                        LOG.debug(installResult.getError().getMessage()
                                + (result != null ? " (" + result + "/" + v + ")" : ""));
                    } else {
                        LOG.debug("Installed " + v + " from " + url);
                    }
                } else if (installResult.hasError()) {
                    LOG.debug(installResult.getError().getMessage());
                }
            }
        }
        LOG.debug("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(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: no valid manifest"));

            String versionedId = getVersionedId(manifest);
            URL bundleUrl = ResourceUtils.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);
            LOG.debug("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));
    }

    private static String getVersionedId(Bundle b) {
        return b.getSymbolicName() + ":" + b.getVersion();
    }

    private static String getVersionedId(Manifest manifest) {
        Attributes atts = manifest.getMainAttributes();
        return atts.getValue(Constants.BUNDLE_SYMBOLICNAME) + ":" + atts.getValue(Constants.BUNDLE_VERSION);
    }

    /**
     * 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);
        String versionedId = getVersionedId(manifest);
        for (Bundle installedBundle : framework.getBundleContext().getBundles()) {
            if (versionedId.equals(getVersionedId(installedBundle))) {
                if (SYSTEM_BUNDLES.contains(versionedId)) {
                    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);
    }

    public static boolean isExtensionBundle(Bundle bundle) {
        String location = bundle.getLocation();
        return location != null && EXTENSION_PROTOCOL.equals(Urls.getProtocol(location));
    }

    /** Takes a string which might be of the form "symbolic-name" or "symbolic-name:version" (or something else entirely)
     * and returns a VersionedName. The versionedName.getVersion() will be null if if there was no version in the input
     * (or returning {@link Maybe#absent()} if not valid, with a suitable error message). */
    public static Maybe<VersionedName> parseOsgiIdentifier(String symbolicNameOptionalWithVersion) {
        if (Strings.isBlank(symbolicNameOptionalWithVersion))
            return Maybe.absent("OSGi identifier is blank");

        String[] parts = symbolicNameOptionalWithVersion.split(":");
        if (parts.length > 2)
            return Maybe.absent("OSGi identifier has too many parts; max one ':' symbol");

        Version v = null;
        if (parts.length == 2) {
            try {
                v = Version.parseVersion(parts[1]);
            } catch (IllegalArgumentException e) {
                return Maybe.absent("OSGi identifier has invalid version string (" + e.getMessage() + ")");
            }
        }

        return Maybe.of(new VersionedName(parts[0], v));
    }

    /**
     * The class is not used, staying for future reference.
     * Remove after OSGi transition is completed.
     */
    public static class ManifestHelper {

        private static ManifestParser parse;
        private Manifest manifest;
        private String source;

        private static final String WIRING_PACKAGE = PackageNamespace.PACKAGE_NAMESPACE;

        public static ManifestHelper forManifestContents(String contents) throws IOException, BundleException {
            ManifestHelper result = forManifest(Streams.newInputStreamWithContents(contents));
            result.source = contents;
            return result;
        }

        public static ManifestHelper forManifest(URL url) throws IOException, BundleException {
            InputStream in = url.openStream();
            ManifestHelper helper = forManifest(in);
            in.close();
            return helper;
        }

        public static ManifestHelper forManifest(InputStream in) throws IOException, BundleException {
            return forManifest(new Manifest(in));
        }

        public static ManifestHelper forManifest(Manifest manifest) throws BundleException {
            ManifestHelper result = new ManifestHelper();
            result.manifest = manifest;
            parse = new ManifestParser(null, null, null, new StringMap(manifest.getMainAttributes()));
            return result;
        }

        public String getSymbolicName() {
            return parse.getSymbolicName();
        }

        public Version getVersion() {
            return parse.getBundleVersion();
        }

        public String getSymbolicNameVersion() {
            return getSymbolicName() + ":" + getVersion();
        }

        public List<String> getExportedPackages() {
            MutableList<String> result = MutableList.of();
            for (BundleCapability c : parse.getCapabilities()) {
                if (WIRING_PACKAGE.equals(c.getNamespace())) {
                    result.add((String) c.getAttributes().get(WIRING_PACKAGE));
                }
            }
            return result;
        }

        @Nullable
        public String getSource() {
            return source;
        }

        public Manifest getManifest() {
            return manifest;
        }
    }

}