org.apache.brooklyn.core.mgmt.ha.OsgiArchiveInstaller.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.brooklyn.core.mgmt.ha.OsgiArchiveInstaller.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 org.apache.brooklyn.core.mgmt.ha;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.brooklyn.api.typereg.ManagedBundle;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.BrooklynVersion;
import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.server.BrooklynServerConfig;
import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry;
import org.apache.brooklyn.core.typereg.BasicManagedBundle;
import org.apache.brooklyn.core.typereg.BundleUpgradeParser.CatalogUpgrades;
import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
import org.apache.brooklyn.core.typereg.RegisteredTypes;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.osgi.BundleMaker;
import org.apache.brooklyn.util.core.osgi.Osgis;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.ReferenceWithError;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.osgi.VersionedName;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.BrooklynVersionSyntax;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.text.VersionComparator;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

// package-private so we can move this one if/when we move OsgiManager
class OsgiArchiveInstaller {

    private static final Logger log = LoggerFactory.getLogger(OsgiArchiveInstaller.class);

    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX = BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX;

    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX = BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX;

    final private OsgiManager osgiManager;
    private ManagedBundle suppliedKnownBundleMetadata;
    private InputStream zipIn;

    private boolean start = true;
    private boolean loadCatalogBom = true;
    private boolean force = false;
    private boolean deferredStart = false;
    private boolean validateTypes = true;

    private File zipFile;
    private boolean isBringingExistingOsgiInstalledBundleUnderBrooklynManagement = false;
    private Manifest discoveredManifest;
    private VersionedName discoveredBomVersionedName;
    OsgiBundleInstallationResult result;

    private ManagedBundle inferredMetadata;
    private final boolean inputStreamSupplied;

    private volatile Predicate<ManagedBundle> blacklistBundlePersistencePredicate;

    OsgiArchiveInstaller(OsgiManager osgiManager, ManagedBundle knownBundleMetadata, InputStream zipIn) {
        this.osgiManager = osgiManager;
        this.suppliedKnownBundleMetadata = knownBundleMetadata;
        this.zipIn = zipIn;
        inputStreamSupplied = zipIn != null;
    }

    public void setStart(boolean start) {
        this.start = start;
    }

    public void setLoadCatalogBom(boolean loadCatalogBom) {
        this.loadCatalogBom = loadCatalogBom;
    }

    public void setForce(boolean force) {
        this.force = force;
    }

    public void setDeferredStart(boolean deferredStart) {
        this.deferredStart = deferredStart;
    }

    public void setValidateTypes(boolean validateTypes) {
        this.validateTypes = validateTypes;
    }

    private ManagementContextInternal mgmt() {
        return (ManagementContextInternal) osgiManager.getManagementContext();
    }

    private synchronized void init() {
        if (result != null) {
            if (zipFile != null || zipIn == null)
                return;
            throw new IllegalStateException(
                    "This installer instance has already been used and the input stream discarded");
        }
        result = new OsgiBundleInstallationResult();
        inferredMetadata = suppliedKnownBundleMetadata == null ? new BasicManagedBundle()
                : suppliedKnownBundleMetadata;
    }

    private synchronized void makeLocalZipFileFromInputStreamOrUrl() {
        Maybe<Bundle> existingOsgiInstalledBundle = Maybe.absent();
        Maybe<ManagedBundle> existingBrooklynInstalledBundle = Maybe.absent();
        if (zipIn == null) {
            if (suppliedKnownBundleMetadata != null) {
                // if no input stream (zipIn), look for a URL and/or a matching bundle
                if (!suppliedKnownBundleMetadata.isNameResolved()) {
                    existingBrooklynInstalledBundle = Maybe.ofDisallowingNull(
                            osgiManager.getManagedBundleFromUrl(suppliedKnownBundleMetadata.getUrl()));
                    if (existingBrooklynInstalledBundle.isPresent()) {
                        // user supplied just a URL (eg brooklyn.libraries), but we recognise it,
                        // so don't try to reload it, just record the info we know about it to retrieve the bundle
                        ((BasicManagedBundle) suppliedKnownBundleMetadata)
                                .setSymbolicName(existingBrooklynInstalledBundle.get().getSymbolicName());
                        ((BasicManagedBundle) suppliedKnownBundleMetadata)
                                .setVersion(existingBrooklynInstalledBundle.get().getSuppliedVersionString());
                    }
                }
                if (existingOsgiInstalledBundle.isAbsent()
                        && suppliedKnownBundleMetadata.getOsgiUniqueUrl() != null) {
                    existingOsgiInstalledBundle = Osgis.bundleFinder(osgiManager.framework)
                            .requiringFromUrl(suppliedKnownBundleMetadata.getOsgiUniqueUrl()).find();
                }
                if (existingOsgiInstalledBundle.isAbsent() && suppliedKnownBundleMetadata.getUrl() != null) {
                    existingOsgiInstalledBundle = Osgis.bundleFinder(osgiManager.framework)
                            .requiringFromUrl(suppliedKnownBundleMetadata.getUrl()).find();
                }
                if (existingOsgiInstalledBundle.isAbsent() && suppliedKnownBundleMetadata.isNameResolved()) {
                    existingOsgiInstalledBundle = Osgis.bundleFinder(osgiManager.framework)
                            .symbolicName(suppliedKnownBundleMetadata.getSymbolicName())
                            .version(suppliedKnownBundleMetadata.getSuppliedVersionString()).find();
                }
                if (existingOsgiInstalledBundle.isPresent()) {
                    if (existingBrooklynInstalledBundle.isAbsent()) {
                        // try to find as brooklyn bundle based on knowledge of OSGi bundle
                        existingBrooklynInstalledBundle = Maybe.ofDisallowingNull(
                                osgiManager.getManagedBundle(new VersionedName(existingOsgiInstalledBundle.get())));
                    }
                    if (suppliedKnownBundleMetadata.getUrl() == null) {
                        // installer did not supply a usable URL, just coords
                        // but bundle is installed at least to OSGi
                        if (existingBrooklynInstalledBundle.isPresent()) {
                            log.debug("Detected bundle " + suppliedKnownBundleMetadata
                                    + " installed to Brooklyn already; no URL or stream supplied, so re-using existing installation");
                            // if bundle is brooklyn-managed simply say "already installed"
                            result.metadata = existingBrooklynInstalledBundle.get();
                            result.setIgnoringAlreadyInstalled();
                            return;

                        } else {
                            // if bundle is not brooklyn-managed we want to make it be so
                            // and for that we need to find a URL.
                            // the getLocation() _might_ be usable, or might be totally opaque;
                            // in tests we rely on the block below (see system:file:) and things
                            // being explicitly set, but in live and rebind deployments the URL
                            // in practice with karaf how we package it is of the form mvn:...
                            // which _does_ work in this block, so we will be able to do most
                            // things which rely on taking osgi-installed bundles into brooklyn mgmt
                            // (and if not don't think it's a big deal, we just uninstall and reinstall
                            // sometimes or fail with a reasonable error message)
                            String candidateUrl = existingOsgiInstalledBundle.get().getLocation();
                            log.debug("Detected bundle " + suppliedKnownBundleMetadata
                                    + " installed to OSGi but not Brooklyn; trying to find a URL to get bundle binary, candidate "
                                    + candidateUrl);
                            if (Strings.isBlank(candidateUrl)) {
                                throw new IllegalArgumentException(
                                        "No input stream available and no URL could be found: no way to promote "
                                                + suppliedKnownBundleMetadata + " from "
                                                + existingOsgiInstalledBundle.get() + " to Brooklyn management");
                            }
                            try {
                                // do this in special try block, not below, so we can give a better error
                                // (the user won't understand the URL)
                                zipIn = ResourceUtils.create(mgmt()).getResourceFromUrl(candidateUrl);
                                isBringingExistingOsgiInstalledBundleUnderBrooklynManagement = true;
                            } catch (Exception e) {
                                Exceptions.propagateIfFatal(e);
                                throw new IllegalArgumentException(
                                        "Could not find binary for already installed OSGi bundle "
                                                + existingOsgiInstalledBundle.get() + " (location " + candidateUrl
                                                + ") when trying to promote " + suppliedKnownBundleMetadata
                                                + " to Brooklyn management",
                                        e);
                            }
                        }
                    }
                } else if (suppliedKnownBundleMetadata.getUrl() == null) {
                    // not installed anywhere and no URL
                    throw new IllegalArgumentException(
                            "No input stream available and no URL could be found: no way to install "
                                    + suppliedKnownBundleMetadata);
                }

                assert zipIn != null || suppliedKnownBundleMetadata
                        .getUrl() != null : "should have found a stream or inferred a URL";

                if (zipIn != null) {
                    // found input stream for existing osgi bundle

                } else if (existingBrooklynInstalledBundle.isAbsent() || force) {
                    // reload
                    String url = suppliedKnownBundleMetadata.getUrl();
                    if (BrooklynVersion.isDevelopmentEnvironment() && url.startsWith("system:file:")) {
                        // in live dists the url is usually mvn: but in dev/test karaf will prefix it with system;
                        // leave the url alone so we correctly dedupe when considering whether to update, but create a zip file
                        // so that things work consistently in dev/test (in particular ClassLoaderUtilsTest passes).
                        // pretty sure we have to do this, even if not replacing the osgi bundle, because we need to
                        // get a handle on the zip file (although we could skip if not doing persistence - but that feels even worse than this!)
                        try {
                            url = Strings.removeFromStart(url, "system:");
                            File zipTemp = new BundleMaker(ResourceUtils.create()).createJarFromClasspathDir(url);
                            zipIn = new FileInputStream(zipTemp);
                        } catch (FileNotFoundException e) {
                            throw Exceptions.propagate(e);
                        }
                    } else {
                        zipIn = ResourceUtils.create(mgmt()).getResourceFromUrl(url,
                                suppliedKnownBundleMetadata.getUrlCredential());
                    }
                } else {
                    // already installed, not forced, just say already installed
                    // (even if snapshot as this is a reference by URL, not uploaded content) 
                    result.metadata = existingBrooklynInstalledBundle.get();
                    result.setIgnoringAlreadyInstalled();
                    return;
                }
            }

            result.bundle = existingOsgiInstalledBundle.orNull();
        }

        zipFile = Os.newTempFile("brooklyn-bundle-transient-" + suppliedKnownBundleMetadata, "zip");
        try {
            FileOutputStream fos = new FileOutputStream(zipFile);
            Streams.copyClose(zipIn, fos);
            try (ZipFile zf = new ZipFile(zipFile)) {
                // validate it is a valid ZIP, otherwise errors are more obscure later.
                // can happen esp if user supplies a file://path/to/folder/ as the URL.openStream returns a list of that folder (!) 
                // the error thrown by the below is useful enough, and caller will wrap with suppliedKnownBundleMetadata details
                zf.entries();
            }
        } catch (Exception e) {
            throw Exceptions.propagate(e);
        } finally {
            Streams.closeQuietly(zipIn);
            zipIn = null;
        }
    }

    private void discoverManifestFromCatalogBom(boolean isCatalogBomRequired) {
        discoveredManifest = new BundleMaker(mgmt()).getManifest(zipFile);
        ZipFile zf = null;
        try {
            try {
                zf = new ZipFile(zipFile);
            } catch (IOException e) {
                throw new IllegalArgumentException("Invalid ZIP/JAR archive: " + e);
            }
            ZipEntry bom = zf.getEntry(BasicBrooklynCatalog.CATALOG_BOM);
            if (bom == null) {
                bom = zf.getEntry("/" + BasicBrooklynCatalog.CATALOG_BOM);
            }
            if (bom == null) {
                if (isCatalogBomRequired) {
                    throw new IllegalArgumentException("Archive must contain a catalog.bom file in the root");
                } else {
                    return;
                }
            }
            String bomS;
            try {
                bomS = Streams.readFullyString(zf.getInputStream(bom));
            } catch (IOException e) {
                throw new IllegalArgumentException("Error reading catalog.bom from ZIP/JAR archive: " + e);
            }
            discoveredBomVersionedName = BasicBrooklynCatalog
                    .getVersionedName(BasicBrooklynCatalog.getCatalogMetadata(bomS), false);
        } finally {
            Streams.closeQuietly(zf);
        }
    }

    private void updateManifestFromAllSourceInformation() {
        if (discoveredBomVersionedName != null) {
            matchSetOrFail("catalog.bom in archive", discoveredBomVersionedName.getSymbolicName(),
                    discoveredBomVersionedName.getVersionString());
        }

        boolean manifestNeedsUpdating = false;
        if (discoveredManifest == null) {
            discoveredManifest = new Manifest();
            manifestNeedsUpdating = true;
        }
        if (!matchSetOrFail("MANIFEST.MF in archive",
                discoveredManifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME),
                discoveredManifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION))) {
            manifestNeedsUpdating = true;
            discoveredManifest.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME,
                    inferredMetadata.getSymbolicName());
            discoveredManifest.getMainAttributes().putValue(Constants.BUNDLE_VERSION,
                    inferredMetadata.getOsgiVersionString());
        }
        if (Strings.isBlank(inferredMetadata.getSymbolicName())) {
            throw new IllegalArgumentException("Missing bundle symbolic name in BOM or MANIFEST");
        }
        if (Strings.isBlank(inferredMetadata.getSuppliedVersionString())) {
            throw new IllegalArgumentException("Missing bundle version in BOM or MANIFEST");
        }
        if (discoveredManifest.getMainAttributes().getValue(Attributes.Name.MANIFEST_VERSION) == null) {
            discoveredManifest.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(),
                    BasicBrooklynCatalog.OSGI_MANIFEST_VERSION_VALUE);
            manifestNeedsUpdating = true;
        }
        if (manifestNeedsUpdating) {
            File zf2 = new BundleMaker(mgmt()).copyAddingManifest(zipFile, discoveredManifest);
            zipFile.delete();
            zipFile = zf2;
        }
    }

    private synchronized void close() {
        if (zipFile != null) {
            zipFile.delete();
            zipFile = null;
        }
    }

    /**
     * Installs a bundle, taking from ZIP input stream if supplied, falling back to URL in the {@link ManagedBundle} metadata supplied.
     * It will take metadata from any of: a MANIFEST.MF in the ZIP; a catalog.bom in the ZIP; the {@link ManagedBundle} metadata supplied.
     * If metadata is supplied in multiple such places, it must match.
     * Appropriate metadata will be added to the ZIP and installation attempted.
     * <p>
     * If a matching bundle is already installed, the installation will stop with a {@link ResultCode#IGNORING_BUNDLE_AREADY_INSTALLED}
     * unless the bundle is a snapshot or "force" is specified.
     * In the latter two cases, if there is an installed matching bundle, that bundle will be updated with the input stream here,
     * with any catalog items from the old bundle removed and those from this bundle installed.
     * <p>
     * Default behaviour is {@link #setLoadCatalogBom(boolean)} true and {@link #setForce(boolean)} false.
     * <p>
     * The return value is extensive but should be self-evident, and will include a list of any registered types (catalog items) installed. 
     */
    public ReferenceWithError<OsgiBundleInstallationResult> install() {
        boolean startedInstallation = false;

        try {
            init();

            // Before even trying to find or download the bundle, check if it is supposed to be forcibly replaced.
            // If so, return the replacement (if any).
            if (suppliedKnownBundleMetadata != null) {
                if (suppliedKnownBundleMetadata.isNameResolved()) {
                    Maybe<VersionedName> forcedReplacementBundle = CatalogUpgrades
                            .tryGetBundleForcedReplaced(mgmt(), suppliedKnownBundleMetadata.getVersionedName());
                    if (forcedReplacementBundle.isPresent()) {
                        return generateForciblyRemovedResult(suppliedKnownBundleMetadata.getVersionedName(),
                                forcedReplacementBundle);
                    }
                } else if (suppliedKnownBundleMetadata.getUrl() != null
                        && suppliedKnownBundleMetadata.getUrl().toLowerCase().startsWith("mvn:")) {
                    // This inference is not guaranteed to get the right answer - you can put whatever 
                    // you want in the MANIFEST.MF. Also, the maven-bundle-plugin does some surprising
                    // transforms, but we take a simpler approach here.
                    // If folk want it to work for such edge-cases, they should include the 
                    // name:version explicitly in the `brooklyn.libraries list`.
                    Optional<VersionedName> inferredName = inferBundleNameFromMvnUrl(
                            suppliedKnownBundleMetadata.getUrl());
                    if (inferredName.isPresent()) {
                        Maybe<VersionedName> forcedReplacementBundle = CatalogUpgrades
                                .tryGetBundleForcedReplaced(mgmt(), inferredName.get());
                        if (forcedReplacementBundle.isPresent()) {
                            return generateForciblyRemovedResult(inferredName.get(), forcedReplacementBundle);
                        }
                    }
                }
            }

            makeLocalZipFileFromInputStreamOrUrl();
            if (result.code != null)
                return ReferenceWithError.newInstanceWithoutError(result);
            discoverManifestFromCatalogBom(false);
            if (result.code != null)
                return ReferenceWithError.newInstanceWithoutError(result);
            updateManifestFromAllSourceInformation();
            if (result.code != null)
                return ReferenceWithError.newInstanceWithoutError(result);
            assert inferredMetadata.isNameResolved() : "Should have resolved " + inferredMetadata;
            assert inferredMetadata instanceof BasicManagedBundle : "Only BasicManagedBundles supported";
            ((BasicManagedBundle) inferredMetadata).setChecksum(getChecksum(new ZipFile(zipFile)));

            final boolean updating;
            result.metadata = osgiManager.getManagedBundle(inferredMetadata.getVersionedName());
            if (result.getMetadata() != null) {

                // already have a managed bundle - check if this is using a new/different URL
                if (suppliedKnownBundleMetadata != null && suppliedKnownBundleMetadata.getUrl() != null) {
                    String knownIdForThisUrl = osgiManager.managedBundlesRecord
                            .getManagedBundleIdFromUrl(suppliedKnownBundleMetadata.getUrl());
                    if (knownIdForThisUrl == null) {
                        // it's a new URL, but a bundle we already know about
                        log.warn("Request to install from " + suppliedKnownBundleMetadata.getUrl()
                                + " which is not recognized but " + "appears to match " + result.getMetadata()
                                + "; now associating with the latter");
                        osgiManager.managedBundlesRecord.setManagedBundleUrl(suppliedKnownBundleMetadata.getUrl(),
                                result.getMetadata().getId());
                    } else if (!knownIdForThisUrl.equals(result.getMetadata().getId())) {
                        log.warn("Request to install from " + suppliedKnownBundleMetadata.getUrl()
                                + " which is associated to " + knownIdForThisUrl + " but " + "appears to match "
                                + result.getMetadata() + "; now associating with the latter");
                        osgiManager.managedBundlesRecord.setManagedBundleUrl(suppliedKnownBundleMetadata.getUrl(),
                                result.getMetadata().getId());
                    }
                }

                result.bundle = osgiManager.framework.getBundleContext()
                        .getBundle(result.getMetadata().getOsgiUniqueUrl());

                // Check if exactly this bundle is already installed
                if (result.bundle != null && checksumsMatch(result.getMetadata(), inferredMetadata)) {
                    // e.g. repeatedly installing the same bundle
                    log.trace("Bundle " + inferredMetadata + " matches already installed managed bundle "
                            + result.getMetadata() + "; install is no-op");
                    result.setIgnoringAlreadyInstalled();
                    return ReferenceWithError.newInstanceWithoutError(result);
                } else if (isEquivalentBundleAlreadyOsgiInstalled(osgiManager, inferredMetadata, zipFile)) {
                    // e.g. happens if pre-installed bundle is brought under management, and then add it again via a mvn-style url.
                    // We wouldn't know the checksum from the pre-installed bundle.
                    log.trace("Bundle " + inferredMetadata + " matches metadata of managed bundle "
                            + result.getMetadata() + " (but not OSGi bundle location "
                            + result.getMetadata().getOsgiUniqueUrl() + "), "
                            + "and matches already installed OSGi bundle; ; install is no-op");
                    result.setIgnoringAlreadyInstalled();
                    return ReferenceWithError.newInstanceWithoutError(result);
                }

                if (canUpdate()) {
                    if (result.getBundle() == null) {
                        log.warn("Brooklyn thought it was already managing bundle "
                                + result.getMetadata().getVersionedName()
                                + " but it's not installed to framework at location "
                                + result.getMetadata().getOsgiUniqueUrl() + "; reinstalling it");
                        updating = false;
                    } else {
                        log.trace("Updating existing brooklyn-managed bundle " + result);
                        updating = true;
                    }
                } else {
                    List<Bundle> existingBundles = findBundlesByVersion(osgiManager, inferredMetadata);
                    if (existingBundles.size() > 0 && (result.getMetadata().getChecksum() == null
                            || inferredMetadata.getChecksum() == null)) {
                        // e.g. Checksum would be missing if we brought under management a pre-installed bundle with an unusable url.
                        log.info("Missing bundle checksum data for " + result
                                + "; assuming bundle matches existing brooklyn-managed bundle (not re-installing)");
                        result.setIgnoringAlreadyInstalled();
                        return ReferenceWithError.newInstanceWithoutError(result);
                    } else if (result.bundle != null || existingBundles.size() > 0) {
                        throw new IllegalArgumentException(
                                "Bundle " + result.getMetadata().getVersionedName() + " already installed; "
                                        + "cannot install a different bundle with the same non-snapshot version");
                    } else {
                        throw new IllegalArgumentException("Bundle " + result.getMetadata().getVersionedName()
                                + " already a brooklyn-managed bundle, but not found in OSGi framework; "
                                + "will not re-install without use of 'force'");
                    }
                }
            } else {
                // No such Brooklyn-managed bundle.

                // Check if likely-looking bundle already installed to OSGi subsystem, but brooklyn not aware of it.
                // This will often happen on a karaf restart where bundle was cached by karaf, so we need to allow it;
                // can also happen if brooklyn.libraries references an existing bundle.
                //
                // If we're not certain that the bundle is identical 

                // First check if this bundle is forcibly removed (and optionally upgraded).
                // If so, don't install it - return the replacement, if any.
                Maybe<VersionedName> forcedReplacementBundle = CatalogUpgrades.tryGetBundleForcedReplaced(mgmt(),
                        inferredMetadata.getVersionedName());
                if (forcedReplacementBundle.isPresent()) {
                    return generateForciblyRemovedResult(inferredMetadata.getVersionedName(),
                            forcedReplacementBundle);
                }

                result.metadata = inferredMetadata;

                // search for already-installed bundles.
                List<Bundle> existingBundles = findBundlesByVersion(osgiManager, inferredMetadata);
                Maybe<Bundle> existingEquivalentBundle = tryFindEquivalentBundle(existingBundles, inferredMetadata,
                        zipFile);

                if (existingEquivalentBundle.isPresent()) {
                    // Identical bundle (by osgi location or binary content) already installed; just bring that under management.
                    // This will often happen on a karaf restart: bundles from persisted state match those cached by karaf,
                    isBringingExistingOsgiInstalledBundleUnderBrooklynManagement = true;
                    result.bundle = existingEquivalentBundle.get();

                } else if (existingBundles.size() > 0) {
                    Bundle existingBundle = existingBundles.get(0);

                    if (force) {
                        if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
                            log.debug("Request to install " + inferredMetadata
                                    + " was forced, so forcing reinstallation "
                                    + "of existing OSGi installed (but not Brooklyn-managed) bundle "
                                    + existingBundle);
                            isBringingExistingOsgiInstalledBundleUnderBrooklynManagement = false;
                        }
                    }

                    if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
                        // We were explicitly asked to bring an existing OSGi bundle under management; 
                        // no equivalence check required
                        result.bundle = existingBundle;
                    } else {
                        // Uninstall and re-install the bundle.
                        // This is a good idea for brooklyn managed bundles that were in the karaf cache (when we can't 
                        // determine that they are definitely identical).
                        // It's less good for pre-installed bundles, but if the user has said to deploy it or has
                        // referenced it in `brooklyn.libraries` then we'll go for it anyway! Let's hope they didn't 
                        // reference `org.apache.brooklyn.core` or some such.
                        // 
                        // We are this extreme because we want rebind to always work! If a user did a `force` install
                        // of a bundle, then we want to do the same on rebind (rather than risk failing).
                        //
                        // Instead of uninstall, we could update the bundle.
                        // Note however either way we won't be able to rollback if there is a failure
                        log.debug("Brooklyn install of " + result.getMetadata().getVersionedName()
                                + " detected already loaded in OSGi; uninstalling that to reinstall as Brooklyn-managed");
                        existingBundle.uninstall();
                        result.bundle = null;
                    }
                }

                updating = false;
            }

            startedInstallation = true;
            try (InputStream fin = new FileInputStream(zipFile)) {
                if (!updating) {
                    if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
                        assert result.getBundle() != null;
                        log.debug("Brooklyn install of " + result.getMetadata().getVersionedName()
                                + " detected already loaded " + result.getBundle()
                                + " in OSGi can be re-used, skipping OSGi install");
                    } else {
                        assert result.getBundle() == null;
                        log.debug("Installing bundle " + result.getMetadata().getVersionedName()
                                + ", using OSGi location " + result.getMetadata().getOsgiUniqueUrl());
                        result.bundle = osgiManager.framework.getBundleContext()
                                .installBundle(result.getMetadata().getOsgiUniqueUrl(), fin);
                    }
                } else {
                    result.bundle.update(fin);
                }
            }

            osgiManager.checkCorrectlyInstalled(result.getMetadata(), result.bundle);
            final File oldZipFile;

            if (!updating) {
                oldZipFile = null;
                osgiManager.managedBundlesRecord.addManagedBundle(result, zipFile);
                result.code = OsgiBundleInstallationResult.ResultCode.INSTALLED_NEW_BUNDLE;
                result.message = "Installed Brooklyn catalog bundle " + result.getMetadata().getVersionedName()
                        + " with ID " + result.getMetadata().getId() + " [" + result.bundle.getBundleId() + "]";
                if (!isBlacklistedForPersistence(result.getMetadata())) {
                    ((BasicManagedBundle) result.getMetadata()).setPersistenceNeeded(true);
                    mgmt().getRebindManager().getChangeListener().onManaged(result.getMetadata());
                }
            } else {
                oldZipFile = osgiManager.managedBundlesRecord.updateManagedBundleFile(result, zipFile);
                result.code = OsgiBundleInstallationResult.ResultCode.UPDATED_EXISTING_BUNDLE;
                result.message = "Updated Brooklyn catalog bundle " + result.getMetadata().getVersionedName()
                        + " as existing ID " + result.getMetadata().getId() + " [" + result.bundle.getBundleId()
                        + "]";
                if (!isBlacklistedForPersistence(result.getMetadata())) {
                    ((BasicManagedBundle) result.getMetadata()).setPersistenceNeeded(true);
                    mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
                }
            }
            log.debug(result.message
                    + " (partial): OSGi bundle installed, with bundle start and Brooklyn management to follow");
            // can now delete and close (copy has been made and is available from OsgiManager)
            zipFile.delete();
            zipFile = null;

            // setting the above before the code below means if there is a problem starting or loading catalog items
            // a user has to remove then add again, or forcibly reinstall;
            // that seems fine and probably better than allowing bundles to start and catalog items to be installed 
            // when brooklyn isn't aware it is supposed to be managing it

            // starting here flags wiring issues earlier
            // but may break some things running from the IDE
            // eg if it doesn't have OSGi deps, or if it doesn't have camp parser,
            // or if caller is installing multiple things that depend on each other
            // eg rebind code, brooklyn.libraries list -- deferred start allows caller to
            // determine whether not to start or to start all after things are installed
            Runnable startRunnable = new Runnable() {
                private void rollbackBundle() {
                    if (updating) {
                        if (oldZipFile == null) {
                            throw new IllegalStateException("Did not have old ZIP file to install");
                        }
                        log.debug("Rolling back bundle " + result.getVersionedName() + " to state from "
                                + oldZipFile);
                        try {
                            File zipFileNow = osgiManager.managedBundlesRecord.rollbackManagedBundleFile(result,
                                    oldZipFile);
                            result.bundle.update(new FileInputStream(Preconditions.checkNotNull(zipFileNow,
                                    "Couldn't find contents of old version of bundle")));
                        } catch (Exception e) {
                            Exceptions.propagateIfFatal(e);
                            log.error("Error rolling back following failed install of updated "
                                    + result.getVersionedName() + "; "
                                    + "installation will likely be corrupted and correct version should be manually installed.",
                                    e);
                        }

                        if (!isBlacklistedForPersistence(result.getMetadata())) {
                            ((BasicManagedBundle) result.getMetadata()).setPersistenceNeeded(true);
                            mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
                        }
                    } else {
                        if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
                            log.debug("Uninstalling bundle " + result.getVersionedName()
                                    + " from Brooklyn management only (rollback needed but it was already installed to OSGi)");
                        } else {
                            log.debug("Uninstalling bundle " + result.getVersionedName()
                                    + " (roll back of failed fresh install, no previous version to revert to)");
                        }
                        osgiManager.uninstallUploadedBundle(result.getMetadata(), false,
                                isBringingExistingOsgiInstalledBundleUnderBrooklynManagement);
                        if (!isBlacklistedForPersistence(result.getMetadata())) {
                            ((BasicManagedBundle) result.getMetadata()).setPersistenceNeeded(true);
                            mgmt().getRebindManager().getChangeListener().onUnmanaged(result.getMetadata());
                        }
                    }
                }

                public void run() {
                    if (start) {
                        try {
                            log.debug("Starting bundle " + result.getVersionedName());
                            result.bundle.start();
                        } catch (BundleException e) {
                            log.warn("Error starting bundle " + result.getVersionedName()
                                    + ", uninstalling, restoring any old bundle, then re-throwing error: " + e);
                            try {
                                rollbackBundle();
                            } catch (Throwable t) {
                                Exceptions.propagateIfFatal(t);
                                log.warn("Error rolling back " + result.getVersionedName()
                                        + " after bundle start problem; server may be in inconsistent state (swallowing this error and propagating installation error): "
                                        + Exceptions.collapseText(t), t);
                                throw Exceptions.propagate(new BundleException(
                                        "Failure installing and rolling back; server may be in inconsistent state regarding bundle "
                                                + result.getVersionedName() + ". " + "Rollback failure ("
                                                + Exceptions.collapseText(t)
                                                + ") detailed in log. Installation error is: "
                                                + Exceptions.collapseText(e),
                                        e));
                            }

                            throw Exceptions.propagate(e);
                        }
                    }

                    if (loadCatalogBom) {
                        Iterable<RegisteredType> itemsFromOldBundle = null;
                        Map<RegisteredType, RegisteredType> itemsReplacedHere = null;
                        try {
                            if (updating) {
                                itemsFromOldBundle = osgiManager
                                        .uninstallCatalogItemsFromBundle(result.getVersionedName());
                                // (ideally removal and addition would be atomic)
                            }
                            itemsReplacedHere = MutableMap.of();
                            osgiManager.loadCatalogBom(result.bundle, force, validateTypes, itemsReplacedHere);
                            Iterable<RegisteredType> items = mgmt().getTypeRegistry()
                                    .getMatching(RegisteredTypePredicates.containingBundle(result.getMetadata()));
                            log.debug("Adding items from bundle " + result.getVersionedName() + ": " + items);
                            for (RegisteredType ci : items) {
                                result.addType(ci);
                            }
                        } catch (Exception e) {
                            // unable to install new items; rollback bundles
                            // and reload replaced items

                            if (CatalogInitialization.isRebindReadOnlyShuttingDown(osgiManager.mgmt)) {
                                // very likely when RO mode interrupted - ignore
                                throw Exceptions.propagate(e);
                            }

                            log.warn("Error adding Brooklyn items from bundle " + result.getVersionedName()
                                    + ", uninstalling, restoring any old bundle and items, then re-throwing error: "
                                    + Exceptions.collapseText(e));
                            try {
                                rollbackBundle();
                            } catch (Throwable t) {
                                Exceptions.propagateIfFatal(t);
                                log.warn("Error rolling back " + result.getVersionedName()
                                        + " after catalog install problem; server may be in inconsistent state (swallowing this error and propagating installation error): "
                                        + Exceptions.collapseText(t), t);
                                throw Exceptions.propagate(new BundleException(
                                        "Failure loading catalog items, and also failed rolling back; server may be in inconsistent state regarding bundle "
                                                + result.getVersionedName() + ". " + "Rollback failure ("
                                                + Exceptions.collapseText(t)
                                                + ") detailed in log. Installation error is: "
                                                + Exceptions.collapseText(e),
                                        e));
                            }
                            if (itemsFromOldBundle != null) {
                                // add back all itemsFromOldBundle (when replacing a bundle)
                                for (RegisteredType oldItem : itemsFromOldBundle) {
                                    if (log.isTraceEnabled()) {
                                        log.trace("RESTORING replaced bundle item " + oldItem + "\n"
                                                + RegisteredTypes.getImplementationDataStringForSpec(oldItem));
                                    }
                                    ((BasicBrooklynTypeRegistry) mgmt().getTypeRegistry())
                                            .addToLocalUnpersistedTypeRegistry(oldItem, true);
                                }
                            }
                            if (itemsReplacedHere != null) {
                                // and restore any items from other bundles (eg wrappers) that were replaced
                                MutableList<RegisteredType> replaced = MutableList
                                        .copyOf(itemsReplacedHere.values());
                                // in reverse order so if other bundle adds multiple we end up with the real original
                                Collections.reverse(replaced);
                                for (RegisteredType oldItem : replaced) {
                                    if (oldItem != null) {
                                        if (log.isTraceEnabled()) {
                                            log.trace("RESTORING replaced external item " + oldItem + "\n"
                                                    + RegisteredTypes.getImplementationDataStringForSpec(oldItem));
                                        }
                                        ((BasicBrooklynTypeRegistry) mgmt().getTypeRegistry())
                                                .addToLocalUnpersistedTypeRegistry(oldItem, true);
                                    }
                                }
                            }

                            throw Exceptions.propagate(e);
                        }
                    }
                }
            };
            if (deferredStart) {
                result.deferredStart = startRunnable;
                log.debug(result.message + " (Brooklyn load deferred)");
            } else {
                startRunnable.run();
                if (!result.typesInstalled.isEmpty()) {
                    // show fewer info messages, only for 'interesting' and non-deferred installations
                    // (rebind is deferred, as are tests, but REST is not)
                    final int MAX_TO_LIST_EXPLICITLY = 5;
                    Iterable<String> firstN = Iterables.transform(
                            MutableList.copyOf(Iterables.limit(result.typesInstalled, MAX_TO_LIST_EXPLICITLY)),
                            new Function<RegisteredType, String>() {
                                @Override
                                public String apply(RegisteredType input) {
                                    return input.getVersionedName().toString();
                                }
                            });
                    log.info(result.message + ", items: " + firstN
                            + (result.typesInstalled.size() > MAX_TO_LIST_EXPLICITLY
                                    ? " (and others, " + result.typesInstalled.size() + " total)"
                                    : ""));
                    if (log.isDebugEnabled() && result.typesInstalled.size() > MAX_TO_LIST_EXPLICITLY) {
                        log.debug(result.message + ", all items: " + result.typesInstalled);
                    }
                } else {
                    log.debug(result.message
                            + " (complete): bundle started and now managed by Brooklyn, though no catalog items found (may have installed other bundles though)");
                }
            }

            return ReferenceWithError.newInstanceWithoutError(result);

        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            result.code = startedInstallation ? OsgiBundleInstallationResult.ResultCode.ERROR_LAUNCHING_BUNDLE
                    : OsgiBundleInstallationResult.ResultCode.ERROR_PREPARING_BUNDLE;
            result.message = "Bundle " + inferredMetadata + " failed "
                    + (startedInstallation ? "installation" : "preparation") + ": " + Exceptions.collapseText(e);
            return ReferenceWithError.newInstanceThrowingError(result,
                    new IllegalStateException(result.message, e));
        } finally {
            close();
        }
    }

    @VisibleForTesting
    static Optional<VersionedName> inferBundleNameFromMvnUrl(String url) {
        // Assumes format 'mvn:<groupId>/<artifactId>/<version>'
        // e.g. "mvn:io.brooklyn.etcd/brooklyn-etcd/2.7.0"
        assert url.startsWith("mvn:") : "url=" + url;
        String[] parts = url.substring(4).split("/");
        if (parts.length != 3)
            return Optional.absent();
        if (parts[0].trim().isEmpty() || parts[1].trim().isEmpty() || parts[2].trim().isEmpty())
            return Optional.absent();
        return Optional.of(new VersionedName(parts[0] + "." + parts[1], parts[2]));
    }

    private ReferenceWithError<OsgiBundleInstallationResult> generateForciblyRemovedResult(
            VersionedName desiredBundle, Maybe<VersionedName> replacementBundle) {
        if (replacementBundle.isPresentAndNonNull()) {
            result.metadata = osgiManager.getManagedBundle(replacementBundle.get());
            if (result.getMetadata() != null) {
                result.bundle = osgiManager.framework.getBundleContext()
                        .getBundle(result.getMetadata().getOsgiUniqueUrl());

                log.debug("Bundle " + inferredMetadata + " forcibly replaced by bundle " + result.getMetadata()
                        + "; install is no-op");
                result.setIgnoringForciblyRemoved(inferredMetadata.getVersionedName(), replacementBundle);
                return ReferenceWithError.newInstanceWithoutError(result);
            } else {
                throw new IllegalArgumentException("Bundle " + inferredMetadata + " forcibly replaced by bundle "
                        + replacementBundle.get() + ", but replacement not found");
            }
        } else {
            log.debug("Bundle " + inferredMetadata + " forcibly removed, but no upgrade bundle supplied"
                    + "; install is no-op");
            result.setIgnoringForciblyRemoved(inferredMetadata.getVersionedName(), Maybe.absent());
            return ReferenceWithError.newInstanceWithoutError(result);
        }
    }

    @VisibleForTesting
    boolean isBlacklistedForPersistence(ManagedBundle managedBundle) {
        // We treat as "managed bundles" (to extract their catalog.bom) the contents of:
        //   - org.apache.brooklyn.core
        //   - org.apache.brooklyn.policy
        //   - org.apache.brooklyn.test-framework
        //   - org.apache.brooklyn.software-*
        //   - org.apache.brooklyn.library-catalog
        //   - org.apache.brooklyn.karaf-init (not sure why this one could end up in persisted state!)
        // We don't want to persist the entire brooklyn distro! Therefore default is to blacklist those.

        if (blacklistBundlePersistencePredicate == null) {
            String whitelistRegex = mgmt().getConfig().getConfig(PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX);
            String blacklistRegex = mgmt().getConfig().getConfig(PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX);

            final Pattern whitelistPattern = (whitelistRegex != null) ? Pattern.compile(whitelistRegex) : null;
            final Pattern blacklistPattern = (blacklistRegex != null) ? Pattern.compile(blacklistRegex) : null;

            blacklistBundlePersistencePredicate = new Predicate<ManagedBundle>() {
                @Override
                public boolean apply(ManagedBundle input) {
                    String bundleName = input.getSymbolicName();
                    if (whitelistPattern != null && whitelistPattern.matcher(bundleName).matches()) {
                        return false;
                    }
                    if (blacklistPattern != null && blacklistPattern.matcher(bundleName).matches()) {
                        return true;
                    }
                    return false;
                }
            };
        }

        return blacklistBundlePersistencePredicate.apply(managedBundle);
    }

    private static List<Bundle> findBundlesByVersion(OsgiManager osgiManager, ManagedBundle desired) {
        return Osgis.bundleFinder(osgiManager.framework).symbolicName(desired.getSymbolicName())
                .version(desired.getOsgiVersionString()).findAll();
    }

    private static boolean checksumsMatch(ManagedBundle actual, ManagedBundle desired) {
        return actual.getChecksum() != null && Objects.equal(actual.getChecksum(), desired.getChecksum());
    }

    private static boolean isEquivalentBundleAlreadyOsgiInstalled(OsgiManager osgiManager, ManagedBundle desired,
            File zipFile) {
        for (Bundle bundle : findBundlesByVersion(osgiManager, desired)) {
            if (isEquivalentBundle(bundle, desired, zipFile)) {
                return true;
            }
        }

        return false;
    }

    private static Maybe<Bundle> tryFindEquivalentBundle(Iterable<? extends Bundle> bundles, ManagedBundle desired,
            File zipFile) {
        for (Bundle bundle : bundles) {
            if (isEquivalentBundle(bundle, desired, zipFile)) {
                return Maybe.of(bundle);
            }
        }

        return Maybe.absent();
    }

    private static boolean isEquivalentBundle(Bundle bundle, ManagedBundle desired, File zipFile) {
        // Would be nice to also use `desired.getChecksum()`, but not clear if we can get
        // MD5 checksum from an installed OSGi bundle.

        if (Objects.equal(bundle.getLocation(), desired.getUrl())) {
            // installation request was for identical location, so assume we are simply bringing under mgmt
            log.debug("Request to install " + desired + " from same location " + bundle.getLocation()
                    + " as existing OSGi installed (but not Brooklyn-managed) bundle " + bundle
                    + ", so skipping reinstall");
            return true;
        } else {
            // different locations, but see if we can compare input stream contents
            // (prevents needless uninstall/reinstall of already installed bundles)
            try {
                if (Streams.compare(new FileInputStream(zipFile), new URL(bundle.getLocation()).openStream())) {
                    log.debug("Request to install " + desired + " has same contents"
                            + " as existing OSGi installed (but not Brooklyn-managed) bundle " + bundle
                            + ", so skipping reinstall");
                    return true;
                } else {
                    log.debug("Request to install " + desired + " has different contents"
                            + " as existing OSGi installed (but not Brooklyn-managed) bundle " + bundle
                            + ", so will do reinstall (if no other equivalents found)");
                    return false;
                }
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                // probably an invalid URL on installed bundle; that's allowed
                log.debug("Request to install " + desired + " could not compare contents"
                        + " with existing OSGi installed (but not Brooklyn-managed) bundle " + bundle
                        + ", so will do reinstall if not other equivalents found (error " + e + " loading from "
                        + bundle.getLocation() + ")");
                return false;
            }
        }
    }

    private static String getChecksum(ZipFile zf) {
        // checksum should ignore time/date stamps on files - just look at entries and contents. also ignore order.
        // (tests fail without time/date is one reason, but really if a person rebuilds a ZIP that is the same 
        // files we should treat it as identical)
        try {
            Map<String, String> entriesToChecksum = MutableMap.of();
            for (ZipEntry ze : Collections.list(zf.entries())) {
                entriesToChecksum.put(ze.getName(), Streams.getMd5Checksum(zf.getInputStream(ze)));
            }
            return Streams.getMd5Checksum(
                    Streams.newInputStreamWithContents(new TreeMap<>(entriesToChecksum).toString()));
        } catch (Exception e) {
            throw Exceptions.propagate(e);
        }
    }

    private boolean canUpdate() {
        // only update if forced, or it's a snapshot for which a byte stream is supplied
        // (IE don't update a snapshot verison every time its URL is referenced in a 'libraries' section)
        return force || (VersionComparator.isSnapshot(inferredMetadata.getSuppliedVersionString())
                && inputStreamSupplied);
    }

    /** true if the supplied name and version are complete; updates if the known data is incomplete;
     * throws if there is a mismatch; false if the supplied data is incomplete */
    private boolean matchSetOrFail(String source, String name, String version) {
        boolean suppliedIsComplete = true;
        if (Strings.isBlank(name)) {
            suppliedIsComplete = false;
        } else if (Strings.isBlank(inferredMetadata.getSymbolicName())) {
            ((BasicManagedBundle) inferredMetadata).setSymbolicName(name);
        } else if (!Objects.equal(inferredMetadata.getSymbolicName(), name)) {
            throw new IllegalArgumentException("Symbolic name mismatch '" + name + "' from " + source
                    + " (expected '" + inferredMetadata.getSymbolicName() + "')");
        }

        if (Strings.isBlank(version)) {
            suppliedIsComplete = false;
        } else if (Strings.isBlank(inferredMetadata.getSuppliedVersionString())) {
            ((BasicManagedBundle) inferredMetadata).setVersion(version);
        } else if (!BrooklynVersionSyntax.equalAsOsgiVersions(inferredMetadata.getSuppliedVersionString(),
                version)) {
            throw new IllegalArgumentException("Bundle version mismatch '" + version + "' from " + source
                    + " (expected '" + inferredMetadata.getSuppliedVersionString() + "')");
        }

        return suppliedIsComplete;
    }

}